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

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

2013-03-20 14:18 1146 查看
声明:这个教程来自这里http://www.raywenderlich.com/4787/how-to-make-a-catapult-shooting-game-with-cocos2d-and-box2d-part-2

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




The squirrels are on a rampage!

这是两篇教程中的第二部分,这里我们将使用cocos2d和box2d来建立一个很酷的弹射游戏。这一部分中,我们将添加可以发射很厉害的炮弹功能到游戏场景中

并且我们将完成整个游戏功能,添加被射击物体和游戏逻辑部分,现在就开始吧。 

建立被射击目标

目标的生成不是非常负责,大多内容我们都已经知道如何去做, 因为要添加很多目标物体,所以让我们来创建一个函数来生成一个刚体,然后可以多次调用这个函数来生成目标物体 .

首先建立一些变量来保留我们的刚体,在头文件中声明变量如下 :

NSMutableSet *targets;
NSMutableSet *enemies;

打开类实现文件,添加下面代码来释放变量  :

[targets release];
[enemies release];

在resetGame函数上面添加一个辅助函数来生成目标物体 :

- (void)createTarget:(NSString*)imageName
atPosition:(CGPoint)position
rotation:(CGFloat)rotation
isCircle:(BOOL)isCircle
isStatic:(BOOL)isStatic
isEnemy:(BOOL)isEnemy
{
CCSprite *sprite = [CCSprite spriteWithFile:imageName];
[self addChild:sprite z:1];

b2BodyDef bodyDef;
bodyDef.type = isStatic?b2_staticBody:b2_dynamicBody;
bodyDef.position.Set((position.x+sprite.contentSize.width/2.0f)/PTM_RATIO,
(position.y+sprite.contentSize.height/2.0f)/PTM_RATIO);
bodyDef.angle = CC_DEGREES_TO_RADIANS(rotation);
bodyDef.userData = sprite;
b2Body *body = world->CreateBody(&bodyDef);

b2FixtureDef boxDef;
if (isCircle)
{
b2CircleShape circle;
circle.m_radius = sprite.contentSize.width/2.0f/PTM_RATIO;
boxDef.shape = &circle;
}
else
{
b2PolygonShape box;
box.SetAsBox(sprite.contentSize.width/2.0f/PTM_RATIO, sprite.contentSize.height/2.0f/PTM_RATIO);
boxDef.shape = &box;
}

if (isEnemy)
{
boxDef.userData = (void*)1;
[enemies addObject:[NSValue valueWithPointer:body]];
}

boxDef.density = 0.5f;
body->CreateFixture(&boxDef);

[targets addObject:[NSValue valueWithPointer:body]];
}

这个函数有很多的参数因为我们将生成各种类型的不同的目标到游戏场景中,别着急,其实这个非常的简单,让我们来搞定它。。

首先我们使用文件名字来加载精灵.,很容易就给这些物体确定坐标,我们以目标体的左下角确定为它的位置,box2d用的位置是位置的中心,素以我们用精灵的尺寸大小来定位它的刚体位置,我们按照我们想要的物体的形状来定义了夹具, 大多数的被射击目标是圆形或者矩形,夹具的尺寸是从精灵尺寸来获得 .

然后,如果是被射击目标的话(这个物体之后将会爆炸,,我还需要保留他们的运动轨迹),我把他们添加到enemies里面保存起来,并且设置userData为1. 这个userData同城会被设置成一个结构体将或者一个指针指向另外一个对象,但是在这里我们想要使用标签出这些夹具是要被被射击的目标,这就是为什么我们要做这个设置的原因,如果一个被射击目标被销毁的时候,我将告诉你怎么来检测它。

然后我们生成了刚体的夹具,并把它添加到targets变量里面保留起来。

现在是调用这个函数来完成我们的游戏场景的时候了,这是一个很大的函数体,因为我不得不调用这个函数很多次来生成每一个目标到游戏场景中。

下面是我们要用的精灵图片 :





现在我们要把这些精灵放到正确的位置上。我是依靠尝试和很多次放错之后才获得他们的正确的位置,你可以直接用下面的代码来搞定这些。把下列代码添加到文件中

- (void)createTargets
{
[targets release]; [enemies release];
targets = [[NSMutableSet alloc] init];
enemies = [[NSMutableSet alloc] init];

// First block
[self createTarget:@"brick_2.png" atPosition:CGPointMake(675.0, FLOOR_HEIGHT) rotation:0.0f isCircle:NO isStatic:NO isEnemy:NO];
[self createTarget:@"brick_1.png" atPosition:CGPointMake(741.0, FLOOR_HEIGHT) rotation:0.0f isCircle:NO isStatic:NO isEnemy:NO];
[self createTarget:@"brick_1.png" atPosition:CGPointMake(741.0, FLOOR_HEIGHT+23.0f) rotation:0.0f isCircle:NO isStatic:NO isEnemy:NO];
[self createTarget:@"brick_3.png" atPosition:CGPointMake(672.0, FLOOR_HEIGHT+46.0f) rotation:0.0f isCircle:NO isStatic:NO isEnemy:NO];
[self createTarget:@"brick_1.png" atPosition:CGPointMake(707.0, FLOOR_HEIGHT+58.0f) rotation:0.0f isCircle:NO isStatic:NO isEnemy:NO];
[self createTarget:@"brick_1.png" atPosition:CGPointMake(707.0, FLOOR_HEIGHT+81.0f) rotation:0.0f isCircle:NO isStatic:NO isEnemy:NO];

[self createTarget:@"head_dog.png" atPosition:CGPointMake(702.0, FLOOR_HEIGHT) rotation:0.0f isCircle:YES isStatic:NO isEnemy:YES];
[self createTarget:@"head_cat.png" atPosition:CGPointMake(680.0, FLOOR_HEIGHT+58.0f) rotation:0.0f isCircle:YES isStatic:NO isEnemy:YES];
[self createTarget:@"head_dog.png" atPosition:CGPointMake(740.0, FLOOR_HEIGHT+58.0f) rotation:0.0f isCircle:YES isStatic:NO isEnemy:YES];

// 2 bricks at the right of the first block
[self createTarget:@"brick_2.png" atPosition:CGPointMake(770.0, FLOOR_HEIGHT) rotation:0.0f isCircle:NO isStatic:NO isEnemy:NO];
[self createTarget:@"brick_2.png" atPosition:CGPointMake(770.0, FLOOR_HEIGHT+46.0f) rotation:0.0f isCircle:NO isStatic:NO isEnemy:NO];

// The dog between the blocks
[self createTarget:@"head_dog.png" atPosition:CGPointMake(830.0, FLOOR_HEIGHT) rotation:0.0f isCircle:YES isStatic:NO isEnemy:YES];

// Second block
[self createTarget:@"brick_platform.png" atPosition:CGPointMake(839.0, FLOOR_HEIGHT) rotation:0.0f isCircle:NO isStatic:YES isEnemy:NO];
[self createTarget:@"brick_2.png" atPosition:CGPointMake(854.0, FLOOR_HEIGHT+28.0f) rotation:0.0f isCircle:NO isStatic:NO isEnemy:NO];
[self createTarget:@"brick_2.png" atPosition:CGPointMake(854.0, FLOOR_HEIGHT+28.0f+46.0f) rotation:0.0f isCircle:NO isStatic:NO isEnemy:NO];
[self createTarget:@"head_cat.png" atPosition:CGPointMake(881.0, FLOOR_HEIGHT+28.0f) rotation:0.0f isCircle:YES isStatic:NO isEnemy:YES];
[self createTarget:@"brick_2.png" atPosition:CGPointMake(909.0, FLOOR_HEIGHT+28.0f) rotation:0.0f isCircle:NO isStatic:NO isEnemy:NO];
[self createTarget:@"brick_1.png" atPosition:CGPointMake(909.0, FLOOR_HEIGHT+28.0f+46.0f) rotation:0.0f isCircle:NO isStatic:NO isEnemy:NO];
[self createTarget:@"brick_1.png" atPosition:CGPointMake(909.0, FLOOR_HEIGHT+28.0f+46.0f+23.0f) rotation:0.0f isCircle:NO isStatic:NO isEnemy:NO];
[self createTarget:@"brick_2.png" atPosition:CGPointMake(882.0, FLOOR_HEIGHT+108.0f) rotation:90.0f isCircle:NO isStatic:NO isEnemy:NO];
}

非常简单的调用这个辅助函数来生成我们想要的精灵,现在把这个函数添加到resetgame函数的最下面。 :

[self createTargets];

如果你现在运行这个项目。你是看不到这一部分,除非你发射一个炮弹直到看到这一部分,让我们添加一行代码到init中。使得我们能够很容易的看到所添加的精灵。

self.position = CGPointMake(-480, 0);

这将给我们展示整个画面的右半部分,现在运行项目你就会看到很多的精灵目标





在进行到下一步之前,如果你想的话可以小玩一下这个场景,例如注释掉最开始的右侧的2个砖块。运行程序看看发生了什么 .

现在在init方法中移除要改变的航,现在来发射一个炮弹看看会如何。





Cool! The squirrel attack is underway!

快速发射炮弹!

在我们进行一些碰撞检测之前,让我们加点代码来把上弹功能加上,这样我们能连续发射炮弹。 .

添加一个新的函数叫resetBullet。在resetGame函数的上方

- (void)resetBullet
{
if ([enemies count] == 0)
{
// game over
// We'll do something here later
}
else if ([self attachBullet])
{
[self runAction:[CCMoveTo actionWithDuration:2.0f position:CGPointZero]];
}
else
{
// We can reset the whole scene here
// Also, let's do this later
}
}

在这个函数里面,我们首先要检测是否我们销毁了所有的被射击目标,目前我们没有销毁他们,所以 暂时不做任何操作,但我们已经准备好这个了 .

如果仍然有很多被射击目标,那我们就把另外一个子弹装到发射臂上,并返回真值,要记得只要还有子弹装,这个函数就要返回真值,否则就返回false。因此只要有子弹,我们就运行action动作来重设场景的位置到左侧,这样我们就可以看到我们的发射臂。 .

如果没子弹了,那我们就必须重新复位整个场景,但是这个处理稍后我们再来处理。让我们先做点好玩的事情先。 .

我们现在要在一个适当的时候来调用这个方法,但是什么时候是适当的时候呢,可能是我们发射了炮弹之后当那个炮弹停止移动的时候?也可能是我们击中了目标之后的几秒?这个要好好讨论讨论了。为了把问题简单话,在我们发射了炮弹之后过几秒钟之后,我们就开始调用这个函数好了。 .

你记得我们在销毁焊接关节之后在tick函数里面做的这个吧,到那个函数里,找到销毁关节的部分,添加以下代码 :

[self performSelector:@selector(resetBullet) withObject:nil afterDelay:5.0f];

这个代码将使得我们发射炮弹以后等5秒钟之后再安装炮弹。现在运行下项目看下结果。

这部分还有最后一件事情,我自己的看法是因为一点细节问题而导致的混乱是不太自然的:这个场景的右边界的物体碰撞都保留在原地就像被一面墙挡住了,但是又没有墙,物体应该倒向屏幕的右侧,但是他们都没有那样

这样的事情发生是因为我么没有建立我们的世界,我们添加了夹具到场景的四个角上,我们现在能看到右角上可能都不存在,因为到init函数里移除一下代码。

// remove these lines under the comment "right"
groundBox.SetAsEdge(b2Vec2(screenSize.width*2.0f/PTM_RATIO,screenSize.height/PTM_RATIO), b2Vec2(screenSize.width*2.0f/PTM_RATIO,0));
groundBody->CreateFixture(&groundBox,0);

现在再运行下,就看到比过去自然了很多。 .





.

雨中的小猫和小狗

我们快完成啦。我们需要一个方法来检测被射击目标应该被销毁。

我们可以用碰撞检测来完成这个功能,但是简单碰撞检测会有一点问题,我们的被射击目标已经被砖块碰撞了,只是检测被射击物体被什么东西碰撞了,这是不够的,因为被射击要被立刻销毁。

我们可以说,如果被射击物体被一个炮弹碰撞之后他就可以被销毁,那就太容易了,但是很多被射击物体很难被销毁,在两个砖块之间的小狗,很难被一个炮弹撞到,但是如果我们仍一个砖块到它身上就不难了。但是我们已经使用砖块完成了一个碰撞,所以这个不太好使。

我能做的就是检测碰撞的力度,然后依靠一个最小力度来决定我们是否销毁被射击目标 ,为了实现这个,我们需要建立一个接触侦听器。如果你不清楚这个可以看之前的文章

这次有点不同。首先我们使用std::代替std:: 不同的是,那不允许如果有多重影响到目标上这个目标可能会被多次添加到我们的销毁列表里。另外一个不同是我们必须用postSolve方法来作为我们能够取得我们接触点的冲力,来决定是否销毁目标。

双击主类目录,点击iso\c and c++,选择c++文件,点击下一步。

名字命名为MyContactListener.cpp,点击保存,如果没有生成..h文件的话。要选择 “Header File” 来生成一个头文件MyContactLister.h

打开刚刚生成的MyContactLister.h 添加下列代码:

#import "Box2D.h"
#import <set>
#import <algorithm>

class MyContactListener : public b2ContactListener {

public:
std::set<b2Body*>contacts;

MyContactListener();
~MyContactListener();

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);

};

这根之前做过的很相似。下一步增加代码到实现文件里面 MyContactListener.cpp :

#include "MyContactListener.h"

MyContactListener::MyContactListener() : contacts()
{
}

MyContactListener::~MyContactListener()
{
}

void MyContactListener::BeginContact(b2Contact* contact)
{
}

void MyContactListener::EndContact(b2Contact* contact)
{
}

void MyContactListener::PreSolve(b2Contact* contact,
const b2Manifold* oldManifold)
{
}

void MyContactListener::PostSolve(b2Contact* contact,
const b2ContactImpulse* impulse)
{
bool isAEnemy = contact->GetFixtureA()->GetUserData() != NULL;
bool isBEnemy = contact->GetFixtureB()->GetUserData() != NULL;
if (isAEnemy || isBEnemy)
{
// Should the body break?
int32 count = contact->GetManifold()->pointCount;

float32 maxImpulse = 0.0f;
for (int32 i = 0; i < count; ++i)
{
maxImpulse = b2Max(maxImpulse, impulse->normalImpulses[i]);
}

if (maxImpulse > 1.0f)
{
// Flag the enemy(ies) for breaking.
if (isAEnemy)
contacts.insert(contact->GetFixtureA()->GetBody());
if (isBEnemy)
contacts.insert(contact->GetFixtureB()->GetBody());
}
}
}

正如之前我提到的,我们只实现postsolve函数。

首先我们决定我们进行处理的至少是涉及到一个目标。 记得在生成这些精灵物体的函数里,我们给每一个目标都.用夹具的数据做了个标记吗?这就是为什么我要做那个标记,我告诉你,你马上就明白怎么回事了。

每次接触都会有一个或者多个接触点,每一个接触点都有一个法向冲量(每个法向冲量是法向力和时间步长的乘积)。这个冲力是每一个接触的压力,我们判定一个碰撞力的最大值,然后再决定是否销毁目标。如果我们判定我们应该销毁这个目标,那么我们就把这个目标的刚体对象添加到我们的set列表里,这样待会我们就可以把它销毁掉。 记得之前的教程里,Ray说过,我们不能在接触进行的时候销毁目标刚体,所以我们把这个要销毁的目标放在一个set列表里,之后我们再来处理它

我用过的销毁目标物体的冲量值,你应该自己测试它,这样依赖于这些物体的属性、炮弹的速度还有其他一些因素,我的建议是开始用最小的值和增加的值来判断适合我们的游戏的值。 .

现在我们已经有了侦听碰撞代码,我们来创建实例并使用它。打开HelloWorldLayer.h 添加头文件

#import "MyContactListener.h"

添加一个变量

MyContactListener *contactListener;

在init函数末尾创建碰撞侦听 :

contactListener = new MyContactListener();
world->SetContactListener(contactListener);

现在我们添加下列代码来销毁被射击目标。到tick函数末尾 :

// Check for impacts
std::set<b2Body*>::iterator pos;
for(pos = contactListener->contacts.begin();
pos != contactListener->contacts.end(); ++pos)
{
b2Body *body = *pos;

CCNode *contactNode = (CCNode*)body->GetUserData();
[self removeChild:contactNode cleanup:YES];
world->DestroyBody(body);

[targets removeObject:[NSValue valueWithPointer:body]];
[enemies removeObject:[NSValue valueWithPointer:body]];
}

// remove everything from the set
contactListener->contacts.clear();

我们简单的重复接触侦听器中的删除列表,t,然后销毁所有的目标刚体和相关联的精灵。我们也从生成的所有被射击目标里面删除它们。最后。我们清除了侦听器中的删除列表。 运行起来看下效果





但是你可能说这貌似少了点东西。一些东西告诉我们我们销毁了这些目标。我们添加一些爆炸效果来给增加点色彩。 .

cocos2d非常容易的使用粒子,这里我不能介绍太多。 如果你想深入的学习,你可以看看rdo和ray的书的14章Cocos2d book. 这里我只是给你演示如果做一个粒子子弹。 .

修改添加下列代码 :

b2Body *body = *pos;

CCNode *contactNode = (CCNode*)body->GetUserData();
CGPoint position = contactNode.position;
[self removeChild:contactNode cleanup:YES];
world->DestroyBody(body);

[targets removeObject:[NSValue valueWithPointer:body]];
[enemies removeObject:[NSValue valueWithPointer:body]];

CCParticleSun* explosion = [[CCParticleSun alloc] initWithTotalParticles:200];
explosion.autoRemoveOnFinish = YES;
explosion.startSize = 10.0f;
explosion.speed = 70.0f;
explosion.anchorPoint = ccp(0.5f,0.5f);
explosion.position = position;
explosion.duration = 1.0f;
[self addChild:explosion z:11];
[explosion release];

We first store the position of the enemy’s sprite so we know where to add the particle. Then we add an instance of CCParticleSun in this position. Pretty easy right? Go ahead and run the game!





怎么样?很酷把。。

CCParticleSun是很多预配置粒子系统子类之一,它来自于cocos2d系统,在xcode按command单击在ccparticlesun类上。你将被带到粒子例子文件的实现文件 CCParticlesExamples.m .这个文件有一套粒子系统的子类,你可以体验他们。你可能想CCParticleExplosion这个类会好些把,光看名字,但是你错了,至少我觉得是,到前面试下一个粒子系统看看都能看到什么。 .

你会发现一个事情就是这个用在这个粒子上的纹理文件,如果你看到它的实现代码,就会发现它用了一个叫fire.png文件。这个文件已经被添加到项目文件很久了。 .

完成触屏

在我们做触屏之前,我们先添加一个函数来完成我们发射完炮弹或者消灭完目标后的初始化, 这非常的容易,因为我们对于这个场景大多都是分开创建的

最好的办法是复位我们的游戏,那就是resetGame函数了,但是如果你简单的做这个,你就会清除场景中旧的的目标。因为我们必须对resetgame函数添加清除工作来解决这个事情。幸运的是我们保留了每一件事情的对象,因为做起来很容易。

到resetGame函数的最开始处。

// Previous bullets cleanup
if (bullets)
{
for (NSValue *bulletPointer in bullets)
{
b2Body *bullet = (b2Body*)[bulletPointer pointerValue];
CCNode *node = (CCNode*)bullet->GetUserData();
[self removeChild:node cleanup:YES];
world->DestroyBody(bullet);
}
[bullets release];
bullets = nil;
}

// Previous targets cleanup
if (targets)
{
for (NSValue *bodyValue in targets)
{
b2Body *body = (b2Body*)[bodyValue pointerValue];
CCNode *node = (CCNode*)body->GetUserData();
[self removeChild:node cleanup:YES];
world->DestroyBody(body);
}
[targets release]; [enemies release];
targets = nil;
enemies = nil;
}

这个非常的简单。我们通过接触列表set我们能生成和删除这些场景中的刚体和相关精灵。如果一开始不调用这个函数,那我们就什么都生成不了。

x现在在合适的时候让我们调用resetgame函数。 如果你记得我们在resetbullet函数里留了一个空白 .这就是我们比较合适的地方放这个代码, 我输入下列代码。

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

运行游戏,你会看到当炮弹或者被射击目标没有的时候,游戏将被重设。我们又可以继续玩游戏了。

让我再添加点东西到游戏当中。当游戏开始的时候,我们不能看到目标因为你不知道我们要消灭的是什么,为了弥补这个,改变下resetgame函数。 在resetgame末尾添加三个函数 :

[self createBullets:4];
[self attachBullet];
[self attachTargets];

Let’s change this a bit and add some action:

[self createBullets:4];
[self createTargets];
[self runAction:[CCSequence actions:
[CCMoveTo actionWithDuration:1.5f position:CGPointMake(-480.0f, 0.0f)],
[CCCallFuncN actionWithTarget:self selector:@selector(attachBullet)],
[CCDelayTime actionWithDuration:1.0f],
[CCMoveTo actionWithDuration:1.5f position:CGPointZero],
nil]];

现在我们创建了炮弹和目标,还有一个系列动作。

首先移动场景到右边,我们能看下我们的咪表。移动结束后,我们就调用函数安装炮弹。

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