您的位置:首页 > 移动开发 > Cocos引擎

使用cocos2d和box2d制作一个简单的弹射游戏 第一部分

2013-03-19 12:08 851 查看
声明:这个教程来自这里http://www.raywenderlich.com/4756/how-to-make-a-catapult-shooting-game-with-cocos2d-and-box2d-part-1

这篇文章是由 iOS Tutorial Team 成员Gustavo Ambrozio, 编写的。Gustavo Ambrozio .是CodeCrop
Software.的创始人。他是一个20年经验的软件工程师,拥有超过3年的ios开发经验
.




Create a cute-a-pult game with Cocos2D and Box2D! :D

在这片文章里面。我们将使用cocos2d和box2d建立一个很酷的弹射游戏。 这里我们将使用ray的可爱的妻子vicki制作的的小狗小猫子弹等图片 .

如何使用旋转关节

如何使用weld joints
如何使用摄像机
如何使用碰撞“force”来来决定清除一个我们要来消灭的目标。
And tons more!

本篇文章假定你已经学过如何使用cocos2d和box2d制作弹球游或者相关知识,或者你也学过之前的如何用cocos2d和box2d制作一个撞球游戏一和二。

那我们开始吧。

用Xcode 点击“Create a new Xcode project”. 选择cocos2d and choose the cocos2d_box2d template 选择下一步.





填写项目名字:cute-a-pult。On the next screen give your project a name and fill out with a company identifier. We’ll call the project “cute-a-pult” –

再下一步选择一个目录来存储我们的项目文件。你不需要建立一个目录,xcode会自动帮你建立的。建议使用source control不过这不是必需的。

当你点击create之后。我们的新项目将建立完成。想看效果的话。直接点击运行,即可看到由模板建立的项目效果。是个点击产生文字卡片的程序。



清除干净项目

开始编码之前。让我们做点清除工作。打开helloworldlayer.h文件删除精灵声明部分 addNewSpriteWithCoords:(CGPoint)p.

打开 HelloWorldLayer.mm完全移除以下三个方法。 :

-(void) addNewSpriteWithCoords:(CGPoint)p (also remove from .h)
- (void)ccTouchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
- (void)accelerometer:(UIAccelerometer*)accelerometer didAccelerate:(UIAcceleration*)acceleration

现在编译我们会得到一个警告在初始化里面。让我们清除下init这个函数。

首先要删除加速器。在这里我们不使用这个功能。

:

// delete this line!
self.isAccelerometerEnabled = YES;

后面清除下面的代码。

// Delete these lines!
CCSpriteBatchNode *batch = [CCSpriteBatchNode batchNodeWithFile:@"blocks.png" capacity:150];
[self addChild:batch z:0 tag:kTagBatchNode];

[self addNewSpriteWithCoords:ccp(screenSize.width/2, screenSize.height/2)];

CCLabelTTF *label = [CCLabelTTF labelWithString:@"Tap screen" fontName:@"Marker Felt" fontSize:32];
[self addChild:label z:0];
[label setColor:ccc3(0,0,255)];
label.position = ccp( screenSize.width/2, screenSize.height-50);

这就改好了。让我们再删掉那些不用的图片等资源文件。打开 resources group删掉 blocks.png

编译下。确定能运行成功。你就会得到一个空项目的:





添加一些精灵。

先添加一些图片到我们的项目。我从vicki的网站上下了这些图片,但是我把这些图片修改了一下来给我们的项目使用。我把发射器的修改了下让它更容易操作 .

从github上下载我们使用的文件。下载文件 。解压缩后把这个目录添加到我们的项目里面 .





现在我们开始添加精灵到项目里面。:让画面跟vicki设计的一样。





打开HelloWorldLayer.mm在m_debugDraw->SetFlags(flags);后面添加下列代码:

CCSprite *sprite = [CCSprite spriteWithFile:@"bg.png"];
sprite.anchorPoint = CGPointZero;
[self addChild:sprite z:-1];

sprite = [CCSprite spriteWithFile:@"catapult_base_2.png"];
sprite.anchorPoint = CGPointZero;
sprite.position = CGPointMake(181.0f, FLOOR_HEIGHT);
[self addChild:sprite z:0];

sprite = [CCSprite spriteWithFile:@"squirrel_1.png"];
sprite.anchorPoint = CGPointZero;
sprite.position = CGPointMake(11.0f, FLOOR_HEIGHT);
[self addChild:sprite z:0];

sprite = [CCSprite spriteWithFile:@"catapult_base_1.png"];
sprite.anchorPoint = CGPointZero;
sprite.position = CGPointMake(181.0f, FLOOR_HEIGHT);
[self addChild:sprite z:9];

sprite = [CCSprite spriteWithFile:@"squirrel_2.png"];
sprite.anchorPoint = CGPointZero;
sprite.position = CGPointMake(240.0f, FLOOR_HEIGHT);
[self addChild:sprite z:9];

sprite = [CCSprite spriteWithFile:@"fg.png"];
sprite.anchorPoint = CGPointZero;
[self addChild:sprite z:10];

现在我们添加了精灵,不过这些精灵还不是虚拟物理世界的一部分。我们只是添加了这些精灵。我们还需要把它们添加到物理世界里。默认的精灵anchor是在精灵的中间部分,我们设置到左下角,这样比较容易的摆放他们。 你可能注意到很多y轴坐标都用了FLOOR_HEIGHT,我们还没有定义它。在很多地方使用它,这样我修改的时候就比较容易。在PTM_RATIO下面定义它:

#define FLOOR_HEIGHT    62.0f

点击运行。下面是运行结果。





.

添加发射器的发射臂

首先在头文件里定义世界和地面刚体。b2World* world;

b2Body *groundBody;

然后在init函数里实现它。

// Define the gravity vector.

b2Vec2 gravity;

gravity.Set(0.0f, -10.0f);

// Do we want to let bodies sleep?

// This will speed up the physics simulation

bool doSleep = true;

// Construct a world object, which will hold and simulate the rigid bodies.

world = new b2World(gravity, doSleep);

world->SetContinuousPhysics(true);

定义地面刚体

b2BodyDef groundBodyDef;

groundBodyDef.position.Set(0, 0);

groundBody = world->CreateBody(&groundBodyDef);

b2EdgeShape groundEdge;

b2FixtureDef boxShapeDef;

boxShapeDef.shape = &groundEdge;

是添加物体到这个世界了。向以前那样我们建立世界的边界。这次我们改变一点。默认的世界是iphone的屏幕。场景的宽度是我们要改变的世界的的宽度的2倍。所以当定义世界的边界的时候,我们要对每一个屏幕宽度都要乘以2倍

我们的世界的地平面也不在屏幕的最底部。修改底部y轴成 FLOOR_HEIGTH/PTM_RATIO. 最终代码应该是这样:

groundEdge.Set(b2Vec2(0,0),b2Vec2(screenSize.width/PTM_RATIO,0));

groundBody->CreateFixture(&boxShapeDef);

groundEdge.Set(b2Vec2(0,0),b2Vec2(0,screenSize.height/PTM_RATIO));

groundBody->CreateFixture(&boxShapeDef);

groundEdge.Set(b2Vec2(0,screenSize.height/PTM_RATIO),b2Vec2(screenSize.width/PTM_RATIO,screenSize.height/PTM_RATIO));

groundBody->CreateFixture(&boxShapeDef);

groundEdge.Set(b2Vec2(screenSize.width/PTM_RATIO,screenSize.height/PTM_RATIO),b2Vec2(screenSize.width/PTM_RATIO,0));

groundBody->CreateFixture(&boxShapeDef);

现在我们来添加发射臂,首先定义刚体和夹具。打开HelloWorldLayer.h添加下列代码。 :

b2Fixture *armFixture;
b2Body *armBody;

打开类的实现文件。添加下面代码到init函数里面::

// Create the catapult's arm
//
CCSprite *arm = [CCSprite spriteWithFile:@"catapult_arm.png"];
[self addChild:arm z: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;
armBody = world->CreateBody(&armBodyDef);

b2PolygonShape armBox;
b2FixtureDef armBoxDef;
armBoxDef.shape = &armBox;
armBoxDef.density = 0.3F;
armBox.SetAsBox(11.0f/PTM_RATIO, 91.0f/PTM_RATIO);
armFixture = armBody->CreateFixture(&armBoxDef);

如果你已经学习过之前我们介绍的box2d的教程,这些你都已经知道了。

我们首先加载发射臂的精灵,然后把它添加到场景里,注意到z坐标值。当我们添加静态的精灵到场景的时候我们通常使用z坐标值,因此这个z坐标值会把发射臂放在发射块的中间。看上去非常不错。 .

你也注意到精灵的位置不太对。那是因为tick方法将设置精灵刚体的正确位置。下面我们来创建一个动态刚体。这里刚体的userdata是很重要的的。这样精灵才会对应刚体。在这我们必须使用刚体的中心位置。我们不能用右下角的位置。 .

下一步将建立夹具来描述刚体的物理特性。它将是一个矩形。我们设置夹具这个矩形比精灵的实际尺寸略微小一些。这是因为如果看下精灵。这个精灵实际比那个发射臂大些。。。。。





在这个图片里。黑色矩形是精灵的尺寸。红色的矩形是我们定义夹具的尺寸。运行程序你将看下面界面 :





使用关节旋转

我们需要限制发射臂的移动,使得它能绕一个点来旋转。限制刚体移动方法就是使用关节。

有一个特殊的关节非常适合我们,那就是公转关节(revolute joint),这个就像把两个物体钉到一个点上,但要允许他们旋转,因此我们能把发射臂钉到地面上来获得我们想要的效果。

打开 HelloWorldLayer.h 添加这个变量:

b2RevoluteJoint *armJoint;

打开类实现文件,在发射臂生成代码后面添加下列代码 :

// Create a joint to fix the catapult to the floor.
//
b2RevoluteJointDef armJointDef;
armJointDef.Initialize(groundBody, armBody, b2Vec2(233.0f/PTM_RATIO, FLOOR_HEIGHT/PTM_RATIO));
armJointDef.enableMotor = true;
armJointDef.enableLimit = true;
armJointDef.motorSpeed  = -1260;
armJointDef.lowerAngle  = CC_DEGREES_TO_RADIANS(9);
armJointDef.upperAngle  = CC_DEGREES_TO_RADIANS(75);
armJointDef.maxMotorTorque = 4800;

armJoint = (b2RevoluteJoint*)world->CreateJoint(&armJointDef);

当生成关节的时候,你必须指定两个刚体和一个关键点,你可能在想,为啥不直接钉在底座上啊,在现实世界里是这样的,但是在刚体世界里是不必的,你必须给底座创建一个刚体才行。给底座要创建一个静态的刚体,我们使用之前创作的地面刚体即可。角度的限制和一个马达组合式的这个发射臂很像现实世界的发射器。

你应该也注意到,我们使用enablemotor和motorspeed和maxmotortorque来把这个马达设置到关节上,这样能让发射臂能够顺时针转动

我也也通过enablelinit和lowerangle和upperangle来设置了关节的局限性,这就使得发射臂能从9度到75之间旋转,这是我们想要的发射臂的移动范围。

我们添加了一个关节让发射臂能向后拉动,当我们释放这个压力的时候马达会把发射臂向前拉,这样就模拟了现实世界的发射行为。 !

马达速度是半径/秒,这可能不是很直观,我所做的就是调整不同的值直到达到我想要的效果 ,你也能一点一点的提高这个值直到达到你想要的发射臂移动速度。最大的马达例句属性是马达所能使用的最大力矩,这个值也是很容易就看到尝试效果。

(我自己尝试的结果最好效果是 把速度值改成-10.最大力矩改成700.不过先不要改。等下一步完成之后再改过来)

运行程序来看画面结果:





转动发射臂

现在我们来移动发射臂,为了完成这个,我们将会用到鼠标关节。如果你以前学过那个弹球游戏的教程,你就应该知道鼠标关节是怎么使用了 .

在box2d中。鼠标关节通过是让一个刚体按一个指定点移动。 这正是我们想要的。这里我先声明一个鼠标关节的变量在头文件HelloWorldLayer.h里:

b2MouseJoint *mouseJoint;

添加touchesBegan 方法到类实现文件里

- (void)ccTouchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
if (mouseJoint != nil) return;

UITouch *myTouch = [touches anyObject];
CGPoint location = [myTouch locationInView:[myTouch view]];
location = [[CCDirector sharedDirector] convertToGL:location];
b2Vec2 locationWorld = b2Vec2(location.x/PTM_RATIO, location.y/PTM_RATIO);

if (locationWorld.x < armBody->GetWorldCenter().x + 50.0/PTM_RATIO)
{
b2MouseJointDef md;
md.bodyA = groundBody;
md.bodyB = armBody;
md.target = locationWorld;
md.maxForce = 2000;

mouseJoint = (b2MouseJoint *)world->CreateJoint(&md);
}
}

当你设置鼠标关节的时候,你必须给它两个刚体参数,第一个刚体参数通常设置为边界刚体,第二个刚体参数设置为我们想要移动的刚体。

参数target是我们想要把关节拉到我们想拉到的地方。我们必须首先把屏幕上的触点转成cocos2d的左边,然后再转成物理世界的左边。如果触点指向发射臂刚体的左侧,那么我们就生成一个关节,50像素的偏移是因为我们允许触点点在发射臂的左侧一点点。 .

最大动力属性将决定着作用在发射臂上,使得发射臂向目标点移动的动力。在这里我们需要让它足够大来抵消公转关节的马达产生的力矩。 .如果你把这个值设置的太小,你就不能把发射臂拉回来。你只能减小公转关节的最大马达力矩或者增大鼠标关键的最大动力来拉动它回去。

我建议多次设置这两个不同的值来观察他们的效果,比如减少最大动力到500,你将看到不能拉动发射臂。再把公转关节的最大例句减少到1000.你将发现你又可以拉动发射臂了。我们把数值设置到我们之前的设置,继续我们的下一步。

完成触屏移动函数

- (void)ccTouchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
if (mouseJoint == nil) return;

UITouch *myTouch = [touches anyObject];
CGPoint location = [myTouch locationInView:[myTouch view]];
location = [[CCDirector sharedDirector] convertToGL:location];
b2Vec2 locationWorld = b2Vec2(location.x/PTM_RATIO, location.y/PTM_RATIO);

mouseJoint->SetTarget(locationWorld);
}

这个方法很简单。获取物理世界中的触屏点,并设置到鼠标关节上 .

触屏结束后我们将释放鼠标关节。让我们在 touchesEnded 这个方法里实现:

- (void)ccTouchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
if (mouseJoint != nil)
{
world->DestroyJoint(mouseJoint);
mouseJoint = nil;
}
}

运行程序我们看下效果





这个速度太快了。控制速度的参数是哪个你还记得吗?是公转关节的马达速度和最大力矩。让我们降低这两个参数值看看效果会如何。 到init函数里面

把马达速度设置成-10(之前是-1260)。你会发现这时候就比较正常了。

armJointDef.motorSpeed  = -10; //-1260;

发射炮弹!

我们建立炮弹的刚体。这样我们才能一个一个的使用它。我们需要一个地方来存放它们。在头文件里让我定义下面这些变量 :

NSMutableArray *bullets;
int currentBullet;

在实现类文件里面添加释放代码 :

[bullets release];

添加一个方法来生成炮弹 :

- (void)createBullets:(int)count
{
currentBullet = 0;
CGFloat pos = 62.0f;

if (count > 0)
{
// delta is the spacing between corns
// 62 is the position o the screen where we want the corns to start appearing
// 165 is the position on the screen where we want the corns to stop appearing
// 30 is the size of the corn
CGFloat delta = (count > 1)?((165.0f - 62.0f - 30.0f) / (count - 1)):0.0f;

bullets = [[NSMutableArray alloc] initWithCapacity:count];
for (int i=0; i<count; i++, pos+=delta)
{
// Create the bullet
//
CCSprite *sprite = [CCSprite spriteWithFile:@"acorn.png"];
[self addChild:sprite z:1];

b2BodyDef bulletBodyDef;
bulletBodyDef.type = b2_dynamicBody;
bulletBodyDef.bullet = true;
bulletBodyDef.position.Set(pos/PTM_RATIO,(FLOOR_HEIGHT+15.0f)/PTM_RATIO);
bulletBodyDef.userData = sprite;
b2Body *bullet = world->CreateBody(&bulletBodyDef);
bullet->SetActive(false);

b2CircleShape circle;
circle.m_radius = 15.0/PTM_RATIO;

b2FixtureDef ballShapeDef;
ballShapeDef.shape = &circle;
ballShapeDef.density = 0.8f;
ballShapeDef.restitution = 0.2f;
ballShapeDef.friction = 0.99f;
bullet->CreateFixture(&ballShapeDef);

[bullets addObject:[NSValue valueWithPointer:bullet]];
}
}
}

这些函数大多你应该已经比较熟悉了。我们的函数将生成子弹的数量的变量,定义子弹和发射器之间的距离。一个细节你应该还没看到过,那就是刚体参数的bullet属性,这个属性告诉box2d,这个刚体将是一个移动速度很快的刚体,因此 box2d将会对更小心一些。 .

box2d手册解释它为什么我们需要标注这样的刚体为bullets。

“Game simulation usually generates a sequence of images that are played at some frame
rate. This is called discrete simulation. In discrete simulation, rigid bodies can move
by a large amount in one time step. If a physics engine doesn't account for the large
motion, you may see some objects incorrectly pass through each other. This effect is
called tunneling."

默认的box2d会使用ccd(连续碰撞检测)来防止动态刚体从隧道穿过边界刚体。这是依靠从他们的老位置到新位置搜索形状来完成的, . 引擎通过搜索和计算这些碰撞中的碰撞次数来寻找新的碰撞。刚体被移动到他们的首次碰撞并随着时间一步一步停止 .

正常情况下ccd不被用做检测动态刚体之间。这是为了提高效率。在很多游戏场景中,你需要一些动态刚体使用ccd功能。例如你可能需要射出一个告诉的子弹到一些遮挡物上,如果不使用ccd的功能,子弹将会穿过遮挡物 .我们现在将创建一个函数去把子弹安装到发射臂上。我们需要声明两个变量来做这件事情。在头文件中添加下面两个变量

b2Body *bulletBody;
b2WeldJoint *bulletJoint;

这个刚刚定义的bullet刚体将保留当前的子弹运行轨道,因此我们待会跟踪他的移动。而另外一个子弹关节将指向一个从子弹和发射臂之间的关节。现在打开实现文件。建立一个新的函数

- (BOOL)attachBullet
{
if (currentBullet < [bullets count])
{
bulletBody = (b2Body*)[[bullets objectAtIndex:currentBullet++] pointerValue];
bulletBody->SetTransform(b2Vec2(230.0f/PTM_RATIO, (155.0f+FLOOR_HEIGHT)/PTM_RATIO), 0.0f);
bulletBody->SetActive(true);

b2WeldJointDef weldJointDef;
weldJointDef.Initialize(bulletBody, armBody, b2Vec2(230.0f/PTM_RATIO,(155.0f+FLOOR_HEIGHT)/PTM_RATIO));
weldJointDef.collideConnected = false;

bulletJoint = (b2WeldJoint*)world->CreateJoint(&weldJointDef);
return YES;
}

return NO;
}

我们首先获得一个指向当前子弹的指针(之后我们会有一个方法回收这个指针),这个SetTransform函数改变这个刚体的中心点,这个位置是发射器的顶端位置,我们设置子弹刚体为激活状态,以致box2d能开始他的物理模拟。

然后我们建立了一个weld关节,这个关节会把两个刚体系在一个我们指定的位置上,并且它们之间不允许移动. 我们设置碰撞链接为false是因为我们不想让子弹和发射臂之间发生碰撞。 如果炮弹设置就返回成功。否则就返回失败,待会检查发射子弹是否发完的时候需要用到这个功能。

下面再建立一个新的函数,初始化一些数据 :

- (void)resetGame
{
[self createBullets:4];
[self attachBullet];
}

把这个函数添加到init函数里面 :

[self resetGame];

运行项目。你会发现有点奇怪。。





这个炮弹房的位置有点偏差,那是因为设置的位置的时候,发射臂设置在9度的位置上。但是在初始化时,发射臂仍然在0度上因为炮弹放在了错误的位置上。为了修正这个错误。我们要把发射臂设置下,改变init函数如下

:

[self performSelector:@selector(resetGame) withObject:nil afterDelay:0.5f];

这将在半秒之后调用这个函数。我们看到现在这个位置好了很多。





如果现在拉动发射臂然后释放,发现子弹没有发射出去。因为它还被绑定到关节上。我们需要一个方法去释放炮弹。那我们就必须销毁关节,什么时候在哪销毁呢? 最好的方法是在tick函数里检测碰撞的时候销毁,我们需要定义一个变量来记录 发射臂是否被释放了。

BOOL releasingArm;

现在到触点结束函数里设置是否发射变量 :

if (armJoint->GetJointAngle() >= CC_DEGREES_TO_RADIANS(20))
{
releasingArm = YES;
}

这就使得当发射臂被释放且角度最小为20度的时候,我们设置这个变量为真值。如果发射臂拉动一点点则不能发射炮弹。

现在到tick函数后面添加下列代码 :

// Arm is being released.
if (releasingArm && bulletJoint)
{
// Check if the arm reached the end so we can return the limits
if (armJoint->GetJointAngle() <= CC_DEGREES_TO_RADIANS(10))
{
releasingArm = NO;

// Destroy joint so the bullet will be free
world->DestroyJoint(bulletJoint);
bulletJoint = nil;

}
}

非常简单对吧?运行下看效果。看到炮弹发射的非常快





I我觉得太快了。所以我们依靠减小公转关节的最大力矩来让它慢下来。在初始化init函数中修改最大力矩值为 .

armJointDef.maxMotorTorque = 700; //4800;

再试一次。会发现效果很好。!

免费的摄像头移动

如果我们让场景随着炮弹的移动而移动看上去效果会非常好。 我们能很容易来修改场景的位置,把下面代码添加到tick函数里面 :

// Bullet is moving.
if (bulletBody && bulletJoint == nil)
{
b2Vec2 position = bulletBody->GetPosition();
CGPoint myPosition = self.position;
CGSize screenSize = [CCDirector sharedDirector].winSize;

// Move the camera.
if (position.x > screenSize.width / 2.0f / PTM_RATIO)
{
myPosition.x = -MIN(screenSize.width * 2.0f - screenSize.width, position.x * PTM_RATIO - screenSize.width / 2.0f);
self.position = myPosition;
}
}

先判断是否有炮弹刚体是否安装在发射臂上,这样才能发射和移动屏幕。我们获得它的位置,判断它是否在屏幕中间的左侧,如果是,我们将改变屏幕的位置,使得炮弹在屏幕的中间。注意到有个MIN函数,用它是因为位置是负值的时候,使得屏幕想左侧移动。运行试下效果,非常的酷。不是吗?



内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐