游戏邦在:
杂志专栏:
gamerboom.com订阅到鲜果订阅到抓虾google reader订阅到有道订阅到QQ邮箱订阅到帮看

如何创造一款类似《割绳子》的游戏(二)

发布时间:2013-11-29 11:13:42 Tags:,,,,

作者:Gustavo Ambrozio

在本系列教程的第一部分中,你创造了游戏的基本场景,并使用Box2D的b2RopeJoint和PatrickC的VRope类去创造绳索。

在今天的第二部分也是最后一部分教程中,你将学会如何割绳子!

开始:切断Verlet!

为了割绳子,你需要追踪用户在屏幕上的碰触并检查碰触是否与绳索产生互动。

当用户在屏幕上拖动手指时,ccTouchesMoved:withEvent:方法会被Cocos2D调用,你可以使用它去检查手指当前的位置以及最后追踪到的位置。之后你可以检查这两个点组成的线是否与你在场景中的任何绳索产生交叉。

为了明确该割掉哪条绳索,它将帮助你更好地理解VRope是如何运行的。

VRope是由一系列VPoint对象(使用VStick对象进行连接)所组成的。所以如果你想要割断VRope,最佳方法便是明确用户手指移动所形成的线是否会与场景中的任何VRope的VStick对象产生交叉。

首先需要做的便是找到一个合理的算法去检测两条线是否交叉。你将在HelloWorldLayer类中执行这一算法。

继续前进并添加如下代码到HelloWorldLayer.mm最后:

-(BOOL)checkLineIntersection:(CGPoint)p1 :( CGPoint)p2 :( CGPoint)p3 :( CGPoint)p4
{
// http://local.wasp.uwa.edu.au/~pbourke/geometry/lineline2d/
CGFloat denominator = (p4.y – p3.y) * (p2.x – p1.x) – (p4.x – p3.x) * (p2.y – p1.y);

// In this case the lines are parallel so you assume they don’t intersect
if (denominator == 0.0f)
return NO;
CGFloat ua = ((p4.x – p3.x) * (p1.y – p3.y) – (p4.y – p3.y) * (p1.x – p3.x)) / denominator;
CGFloat ub = ((p2.x – p1.x) * (p1.y – p3.y) – (p2.y – p1.y) * (p1.x – p3.x)) / denominator;

if (ua >= 0.0 && ua <= 1.0 && ub >= 0.0 && ub <= 1.0)
{
return YES;
}

return NO;
}

因为你并不关心交叉点,只在意线与线之间是否产生交叉,所以你并不需要计算x和y。你只需要检查ua和ub是否在0和1之间便可。

你几乎已经准备好执行ccTouchesMoved:withEvent:方法了。在此之前,先呈现你所需要的一些VRope和VPoint实例变量。

首先你需要呈现VRope的数组。打开VRope.h并在界面组块内部添加如下代码:

@property (nonatomic, readonly) NSArray *sticks;

然后在执行组块内部的VRope.mm中综合属性:

@synthesize sticks = vSticks;

现在你需要呈现VPoint类的坐标。添加以下内容到VPoint.h:

-(CGPoint)point;

并添加如下代码到VPoint.m的最后:

-(CGPoint)point {
return CGPointMake(x, y);
}

你基本上准备好了。现在添加ccTouchesMoved:withEvent:的框架到HelloWorldLayer.mm最后:

-(void)ccTouchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
static CGSize s = [[CCDirector sharedDirector] winSize];

UITouch *touch = [touches anyObject];
CGPoint pt0 = [touch previousLocationInView:[touch view]];
CGPoint pt1 = [touch locationInView:[touch view]];

// Correct Y axis coordinates to cocos2d coordinates
pt0.y = s.height – pt0.y;
pt1.y = s.height – pt1.y;

for (VRope *rope in ropes)
{
for (VStick *stick in rope.sticks)
{
CGPoint pa = [[stick getPointA] point];
CGPoint pb = [[stick getPointB] point];

if ([self checkLineIntersection:pt0 :pt1 :pa :pb])
{
// Cut the rope here
return;
}
}
}
}

这个代码非常简单。对于每条绳索,你检查了用于创造绳索的每个树枝。当你发现一个与线交叉的数值被用户拖动时,你便可以切断绳索并停止循环。

好吧,所以并没有那么简单,你仍需要明确如何切断绳索!

在你再次冲向VRope去添加这一函数钱让我们先好好想想。VRope是受到b2RopeJoint的支持,反过来这一绳索节点在每个终端需要一个实体。当你割断VRope,你会想要将其分成两个VRope对象(游戏邦注:或创造一个新的),而为了做到这点你需要另一个b2RopeJoint和两个新实体。

什么?两个新实体?基于现实条件想一想,这看起来很奇怪。当你割断一条绳索时,两个实体并不会突然存在。

这可能是真的,但是在你的小小Box2D世界,你的绳索并不是真正的绳索!它只是两个实体间的逻辑连接。所以当你割断你的世界中的绳索时,想象你将附加一个非常小且看不见的对象当割痕的末端,所以绳索将保持它们的重量和绳索般的行为。

以下是关于你将执行什么的粗略图解:

stick(from raywenderlich)

stick(from raywenderlich)

这并不是最棒的图像,但是你能够从中有所领悟。以下的代码和解释将让你更加清楚。

前往VRope.h并在-(void)reset之后添加这一方法声明:

-(VRope *)cutRopeInStick:(VStick *)stick newBodyA:(b2Body*)newBodyA newBodyB:(b2Body*)newBodyB;

你将用两个新实体和VStick去供给这一方法。让我们看看它是如何做的。。打开VRope.mm并在reset后添加这一方法:

-(VRope *)cutRopeInStick:(VStick *)stick newBodyA:(b2Body*)newBodyA newBodyB:(b2Body*)newBodyB {

// 1-First, find out where in your array the rope will be cut
int nPoint = [vSticks indexOfObject:stick];

// Instead of making everything again you’ll just use the arrays of
// sticks, points and sprites you already have and split them

// 2-This is the range that defines the new rope
NSRange newRopeRange = (NSRange){nPoint, numPoints-nPoint-1};

// 3-Keep the sticks in a new array
NSArray *newRopeSticks = [vSticks subarrayWithRange:newRopeRange];

// 4-and remove from this object’s array
[vSticks removeObjectsInRange:newRopeRange];

// 5-Same for the sprites
NSArray *newRopeSprites = [ropeSprites subarrayWithRange:newRopeRange];
[ropeSprites removeObjectsInRange:newRopeRange];

// 6-Number of points is always the number of sticks + 1
newRopeRange.length += 1;
NSArray *newRopePoints = [vPoints subarrayWithRange:newRopeRange];
[vPoints removeObjectsInRange:newRopeRange];

// 7-The removeObjectsInRange above removed the last point of
// this rope that now belongs to the new rope. You need to clone
// that VPoint and add it to this rope, otherwise you’ll have a
// wrong number of points in this rope
VPoint *pointOfBreak = [newRopePoints objectAtIndex:0];
VPoint *newPoint = [[VPoint alloc] init];
[newPoint setPos:pointOfBreak.x y:pointOfBreak.y];
[vPoints addObject:newPoint];

// 7-And last: fix the last VStick of this rope to point to this new point
// instead of the old point that now belongs to the new rope
VStick *lastStick = [vSticks lastObject];
[lastStick setPointB:newPoint];
[newPoint release];

// 8-This will determine how long the rope is now and how long the new rope will be
float32 cutRatio = (float32)nPoint / (numPoints – 1);

// 9-Fix my number of points
numPoints = nPoint + 1;

// Position in Box2d world where the new bodies will initially be
b2Vec2 newBodiesPosition = b2Vec2(pointOfBreak.x / PTM_RATIO, pointOfBreak.y / PTM_RATIO);

// Get a reference to the world to create the new joint
b2World *world = newBodyA->GetWorld();

// 10-Re-create the joint used in this VRope since bRopeJoint does not allow
// to re-define the attached bodies
b2RopeJointDef jd;
jd.bodyA = joint->GetBodyA();
jd.bodyB = newBodyB;
jd.localAnchorA = joint->GetLocalAnchorA();
jd.localAnchorB = b2Vec2(0, 0);
jd.maxLength = joint->GetMaxLength() * cutRatio;
newBodyB->SetTransform(newBodiesPosition, 0.0);

b2RopeJoint *newJoint1 = (b2RopeJoint *)world->CreateJoint(&jd); //create joint

// 11-Create the new rope joint
jd.bodyA = newBodyA;
jd.bodyB = joint->GetBodyB();
jd.localAnchorA = b2Vec2(0, 0);
jd.localAnchorB = joint->GetLocalAnchorB();
jd.maxLength = joint->GetMaxLength() * (1 – cutRatio);
newBodyA->SetTransform(newBodiesPosition, 0.0);

b2RopeJoint *newJoint2 = (b2RopeJoint *)world->CreateJoint(&jd); //create joint

// 12-Destroy the old joint and update to the new one
world->DestroyJoint(joint);
joint = newJoint1;

// 13-Finally, create the new VRope
VRope *newRope = [[VRope alloc] initWithRopeJoint:newJoint2
spriteSheet:spriteSheet
points:newRopePoints
sticks:newRopeSticks
sprites:newRopeSprites];
return [newRope autorelease];
}

这真的是一串很长的代码。我将通过另一张图解详细解释上述内容:

illustration(from raywenderlich)

illustration(from raywenderlich)

图解呈现的是构成VRope的VPoints和VSticks。就像你所看到的,VRope拥有7个点和6个树枝。上方是点的指标而下方则是树脂的指标。

在这一情况中,绳索将被从中间割断(nPoint=3)。这一理念是为了使用初始VRope的数据,所以割断行为将会自然地出现。

让我们详细分析上述代码:

1.首先决定割断会出现的点。通过的树枝对象将是属于新绳索的第一个树枝,所以它的指标便是割断发生的点的指标。在上图中便是3。

2.定义一个范围结构去指出你想要vStick数组在哪里分离。在图解中,这一范围将伴随着位置=3,长度=3。这表明了新的绳索在树枝上的范围是从3到5。

3.创造一个伴随着树枝(将转移到新的绳索上)的新数组。

4.将这些树枝从当前绳索的数组中删除。

5.在用于绘制树枝的精灵身上做同样的设置。

6.将长度范围延长1,因为点数总是树枝数+1。基于会出现在新绳索(图解中的点3和点6)的点创造一个新的数组,并将其从这一对象的数组中删除。

7、你可能会注意到在图解中绳索的断裂点被从对象数组中删除了,并转移到了新的绳索。为了改正这点你需要“复制”这一点并将其添加到对象的点数组中。这一对象的最后树枝同样也需要做出更新。

8.割断比例将用于判断新绳索的长度。在图解中,这一比例将为0.5。

9.修改这一对象的点数。在图解中,它变成了4。

10.重新创造节点,让它能够与这一对象联系在一起。附加bodyA和newBodyB到这一绳索的节点上,使用旧的长度和割断比例去决定新长度。同样,将newBodyB放置在绳索被割断的位置。

11.创造一个新的绳索节点并附加newBodyA和bodyB到它上面,同样在割断位置放置newBodyA。

12.然后销毁旧的节点并更新实例变量为新节点。

13.最后,使用你在之前所收集的数组创造新的VRope对象和新的节点。你仍然需要执行这一初始方法,但这却非常简单,紧接着便是这一代码。

这便是初始方法。添加这一内容到你刚刚添加的代码下方:

-(id)initWithRopeJoint:(b2RopeJoint*)aJoint
spriteSheet:(CCSpriteBatchNode*)spriteSheetArg
points:(NSArray*)points
sticks:(NSArray*)sticks
sprites:(NSArray*)sprites {
if((self = [super init])) {
joint = aJoint;
spriteSheet = spriteSheetArg;
vPoints = [[NSMutableArray alloc] initWithArray:points];
vSticks = [[NSMutableArray alloc] initWithArray:sticks];
ropeSprites = [[NSMutableArray alloc] initWithArray:sprites];
numPoints = vPoints.count;
}
return self;
}

是的,这一代码很简单。它只是追踪了你之前进入的数组并更新了numPoints实例变量。

你可能已经注意到在代码中有一个关于方法调用setPointB并未被发现的警告。是的,我骗了你。因为我不希望在解释大量割绳子方法的同时让这些内容复杂化。但这却是很容易解决的问题。

打开VStick.h并添加如下声明:

-(void)setPointB:(VPoint *)point;

然后打开VStick.m并在最后添加如下代码:

-(void)setPointB:(VPoint *)point {
pointB = point;
}

就像你所看到的,这只是一个简单的调节方法。现在你的程序应该可以不带任何问题地进行编译,你最终可以割断绳子了!

不过在你割断绳子前,你需要两个新的实体。在HelloWorldLayer.mm中添加如下新方法去创造这些实体:

-(b2Body *) createRopeTipBody
{
b2BodyDef bodyDef;
bodyDef.type = b2_dynamicBody;
bodyDef.linearDamping = 0.5f;
b2Body *body = world->CreateBody(&bodyDef);

b2FixtureDef circleDef;
b2CircleShape circle;
circle.m_radius = 1.0/PTM_RATIO;
circleDef.shape = &circle;
circleDef.density = 10.0f;

// Since these tips don’t have to collide with anything
// set the mask bits to zero
circleDef.filter.maskBits = 0;
body->CreateFixture(&circleDef);

return body;
}

在这里唯一值得指出的便是maskBits属性。它被设为0所以它不会与你的世界中的任何事物发生碰撞,这也是它应该具有的状态,因为它只是你用于模拟绳索重量的实体。

现在回到ccTouchesMove:withEvent:并在return声明钱添加如下内容:

b2Body *newBodyA = [self createRopeTipBody];
b2Body *newBodyB = [self createRopeTipBody];

VRope *newRope = [rope cutRopeInStick:stick newBodyA:newBodyA newBodyB:newBodyB];
[ropes addObject:newRope];

这应用了你在之前所创造的所有内容,并在正确的点上割断了绳索。

现在这些代码便已足够了。创建并运行,你应该能够用手指割断绳索了。

cut the rope(from raywenderlich)

cut the rope(from raywenderlich)

不过现在乐趣和游戏才真正开始。

吃菠萝的鳄鱼?为什么不?

既然较为复杂的部分已经完成了,让我们添加一些游戏玩法去喂鳄鱼吃菠萝吧。

在精灵表上你已经拥有另一个鳄鱼精灵,即张着大嘴巴。你接下来要做的便是让鳄鱼时不时打开并关上嘴巴。为了让菠萝在鳄鱼张开嘴巴的时候掉落,玩家必须计算绳子被割断的时间、所以是鳄鱼决定自己什么时候饿了!

你需要一种方法去检查掉落的菠萝是否与鳄鱼的嘴巴相吻合。为了做到这点,你需要使用Box2D的接触监听器。

第一步便是添加一个将模拟鳄鱼嘴巴的实体,所以接触监听器将检测与菠萝的碰触。打开HelloWorldLayer.h并添加如下内容:

b2Body *crocMouth_;          // weak ref
b2Fixture *crocMouthBottom_;    // weak ref

然后前往HelloWorldLayer.mm的initPhysics并在最后添加如下代码:

// Define the croc’s “mouth”.
b2BodyDef crocBodyDef;
crocBodyDef.position.Set((s.width – croc_.textureRect.size.width)/PTM_RATIO, (croc_.position.y)/PTM_RATIO);

crocMouth_ = world->CreateBody(&crocBodyDef);

// Define the croc’s box shape.
b2EdgeShape crocBox;

// bottom
crocBox.Set(b2Vec2(5.0/PTM_RATIO,15.0/PTM_RATIO), b2Vec2(45.0/PTM_RATIO,15.0/PTM_RATIO));
crocMouthBottom_ = crocMouth_->CreateFixture(&crocBox,0);

crocMouth_->SetActive(NO);

你最先在精灵的左下角创造了一个静态实体。然后你创造一个固定装置去模拟鳄鱼的嘴巴底部(即你将用于碰撞检测的装置)。如果你运行项目,你便会在右边位置看到这个固定装置。

croc(from raywenderlich)

croc(from raywenderlich)

当你打开鳄鱼的嘴巴时你便能够看到它。你可能会好奇为什么实体(游戏邦注:这是指鳄鱼嘴巴的物理实体而不是真正的实体)被设为暂停(最后一行)。这是因为鳄鱼的嘴巴最初是紧闭着的。在这种状态下,你便不需要菠萝与之互动。

是时候让鳄鱼张开嘴巴了。添加如下两个变量到HelloWorldLayer.h中:

BOOL crocMouthOpened;
NSTimer *crocAttitudeTimer

现在添加这一方法到HelloWorldLayer.mm:

-(void)changeCrocAttitude
{
crocMouthOpened = !crocMouthOpened;
NSString *spriteName = crocMouthOpened ? @”croc_front_mouthopen.png” : @”croc_front_mouthclosed.png”;
[croc_ setDisplayFrame:[[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName:spriteName]];
[croc_ setZOrder:crocMouthOpened ? 1 : -1];

crocMouth_->SetActive(crocMouthOpened);

[crocAttitudeTimer invalidate];
[crocAttitudeTimer release];
crocAttitudeTimer = [[NSTimer scheduledTimerWithTimeInterval:3.0 + 2.0 * CCRANDOM_0_1()
target:self
selector:@selector(changeCrocAttitude)
userInfo:nil
repeats:NO] retain];
}

这一方法改变了布尔运算,即表明鳄鱼是否打开或闭上嘴巴,并改变精灵去反应这一情况,如果嘴巴是张开的,将实体设置为有效,最终在3至5秒间的随机时间后开启计时器去重复这一情况。

注意你正在改变鳄鱼的精灵zOrder。这实现了如下的行为:当鳄鱼张开嘴巴时,如果菠萝掉落,它将落在鳄鱼后面(因为现在菠萝带有一个0的zOrder和1的zOrder),这会给我们一种错觉就好像鳄鱼吃掉了菠萝。如果鳄鱼的嘴巴是闭着的,那么相反的情况便会发生,即菠萝会掉在鳄鱼前面。你将马上看到这种情况。

在你忘记前,在HelloWorldLayer.mm添加如下代码到dealloc:

[crocAttitudeTimer invalidate];
[crocAttitudeTimer release];

最后,在initLevel最后changeCrocAttitude的初始调用:

crocMouthOpened = YES;
[self changeCrocAttitude];

在这之后,changeCrocAttitude将再次在随机间隔中调用方法。

很棒!创建并运行项目,你将看到鳄鱼的嘴巴能够不时打开与闭合了。

eat(from raywenderlich)

eat(from raywenderlich)

如果你在鳄鱼张开嘴巴的时候准确将菠萝掉到它的嘴巴里,它将躺下一会,但是当鳄鱼嘴巴闭上时,我们便会看到菠萝的存在。不要担心,你将使用碰触监听器解决这一问题。

你拥有碰触!

一开始先下载碰触监听器。这里有包含和执行文件。

将这些文件拖到项目树中。确保选中“Copy items into destination group’s folder”将这些文件复制到项目文件夹中:

copy(from raywenderlich)

copy(from raywenderlich)

现在打开HelloWorldLayer.h并添加如下内容:

#import “MyContactListener.h”

并添加如下代码到类变量中:

MyContactListener *contactListener;

打开HelloWorldLayer.mm并在initPhysics最后添加这些内容:

// Create contact listener
contactListener = new MyContactListener();
world->SetContactListener(contactListener);

添加如下清除代码到dealloc:

delete contactListener;
contactListener = NULL;

现在添加这一代码到update的最后(不要担心两个警告和错误的出现,你可以快速地解决它们):

// Check for collisions
bool shouldCloseCrocMouth = NO;
std::vector<b2Body *>toDestroy;
std::vector<MyContact>::iterator pos;
for(pos = contactListener->_contacts.begin(); pos != contactListener->_contacts.end(); ++pos)
{
MyContact contact = *pos;

bool hitTheFloor = NO;
b2Body *potentialCandy = nil;

// The candy can hit the floor or the croc’s mouth. Let’s check
// what it’s touching.
if (contact.fixtureA == crocMouthBottom_)
{
potentialCandy = contact.fixtureB->GetBody();
}
else if (contact.fixtureB == crocMouthBottom_)
{
potentialCandy = contact.fixtureA->GetBody();
}
else if (contact.fixtureA->GetBody() == groundBody)
{
potentialCandy = contact.fixtureB->GetBody();
hitTheFloor = YES;
}
else if (contact.fixtureB->GetBody() == groundBody)
{
potentialCandy = contact.fixtureA->GetBody();
hitTheFloor = YES;
}

// Check if the body was indeed one of the candies
if (potentialCandy && [candies indexOfObject:[NSValue valueWithPointer:potentialCandy]] != NSNotFound)
{
// Set it to be destroyed
toDestroy.push_back(potentialCandy);
if (hitTheFloor)
{
// If it hits the floor you’ll remove all the physics of it and just simulate the pineapple sinking
CCSprite *sinkingCandy = (CCSprite*)potentialCandy->GetUserData();

// Sink the pineapple
CCFiniteTimeAction *sink = [CCMoveBy actionWithDuration:3.0 position:CGPointMake(0, -sinkingCandy.textureRect.size.height)];

// Remove the sprite and check if should finish the level.
CCFiniteTimeAction *finish = [CCCallBlockN actionWithBlock:^(CCNode *node)
{
[self removeChild:node cleanup:YES];
[self checkLevelFinish:YES];
}];

// Run the actions sequentially.
[sinkingCandy runAction:[CCSequence actions:
sink,
finish,
nil]];

// All the physics will be destroyed below, but you don’t want the
// sprite do be removed, so you set it to null here.
potentialCandy->SetUserData(NULL);
}
else
{
shouldCloseCrocMouth = YES;
}
}
}

std::vector<b2Body *>::iterator pos2;
for(pos2 = toDestroy.begin(); pos2 != toDestroy.end(); ++pos2)
{
b2Body *body = *pos2;
if (body->GetUserData() != NULL)
{
// Remove the sprite
CCSprite *sprite = (CCSprite *) body->GetUserData();
[self removeChild:sprite cleanup:YES];
body->SetUserData(NULL);
}

// Iterate though the joins and check if any are a rope
b2JointEdge* joints = body->GetJointList();
while (joints)
{
b2Joint *joint = joints->joint;

// Look in all the ropes
for (VRope *rope in ropes)
{
if (rope.joint == joint)
{
// This “destroys” the rope
[rope removeSprites];
[ropes removeObject:rope];
break;
}
}

joints = joints->next;
world->DestroyJoint(joint);
}

// Destroy the physics body
world->DestroyBody(body);

// Removes from the candies array
[candies removeObject:[NSValue valueWithPointer:body]];
}

if (shouldCloseCrocMouth)
{
// If the pineapple went into the croc’s mouth, immediately closes it.
[self changeCrocAttitude];

// Check if the level should finish
[self checkLevelFinish:NO];
}

这看起来有点复杂,但事实却不是如此,它只是长了点罢。这是用于突围游戏教程中的代码变量。

首先你迭代了所有碰触。如果任何碰触包含了地面或鳄鱼的嘴巴,你便会将参考带到其它实体中,就像它可能是个糖果(游戏邦注:在这一项目中,它不能是任何其它实体,就像绳索并不会与实体或地面碰触到,但是如果现在能够包含这一检测的话便再好不过了,这能防止你在之后添加其它元素到游戏玩法中)。之后你检测了糖果数组,如果潜在糖果实体存在的话你便能够确信它便是糖果。

如果它的确是个糖果的话,你会将其添加到破坏矢量中。在此的区别在于如果它撞到了鳄鱼的嘴巴,你会设置一个弯曲件去闭上鳄鱼的嘴巴。但如果它撞到了地面,那么鳄鱼的嘴巴便不需要闭上了。

在糖果撞到地面的情况下,你将添加动画到精灵上去模拟糖果下降。在模拟最后,你将强迫关卡结束,因为在游戏中,如果鳄鱼吃掉了所有糖果,你便只能通过关卡。(你将很快添加这一方法。)

在首次循环后检查所有碰触,你将通过toDestroy矢量去删除需要被销毁的物理实体。你的游戏中的一个复杂元素是VRope对象的删除,即可能与被删除的对象联系在一起。所以为了所有被销毁的对象,你将通过它的节点并检查它们是否属于任何VRope对象。如果它们属于,你将从场景中删除精灵并从绳索数组中删除VRope对象。

最后,如果遇到了任何嘴巴碰击情况,你便要闭上鳄鱼的嘴巴,并调用一个方法去检查关卡是否应该结束。

现在你应该拥有两个有关checkLevelFinish方法的警告,以及一个错误,因为VRope对象并未揭露节点的实体变量。现在解决这一问题。

你将使用另一个标准C++类,所以添加如下代码到HelloWorldLayer.mm中:

#import <set>

现在添加checkLevelFinish到HelloWorldLayer.mm中:

-(void)checkLevelFinish:(BOOL)forceFinish
{
if ([candies count] == 0 || forceFinish)
{
// Destroy everything
[self finishedLevel];

// Schedule a level restart 2 seconds from now
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, 2.0 * NSEC_PER_SEC);
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
[self initLevel];
});
}
}

-(void) finishedLevel
{
std::set<b2Body *>toDestroy;

// Destroy every rope and add the objects that should be destroyed
for (VRope *rope in ropes)
{
[rope removeSprites];

// Don’t destroy the ground body…
if (rope.joint->GetBodyA() != groundBody)
toDestroy.insert(rope.joint->GetBodyA());
if (rope.joint->GetBodyB() != groundBody)
toDestroy.insert(rope.joint->GetBodyB());

// Destroy the joint already
world->DestroyJoint(rope.joint);
}
[ropes removeAllObjects];

// Destroy all the objects
std::set<b2Body *>::iterator pos;
for(pos = toDestroy.begin(); pos != toDestroy.end(); ++pos)
{
b2Body *body = *pos;
if (body->GetUserData() != NULL)
{
// Remove the sprite
CCSprite *sprite = (CCSprite *) body->GetUserData();
[self removeChild:sprite cleanup:YES];
body->SetUserData(NULL);
}
world->DestroyBody(body);
}

[candies removeAllObjects];
}

checkLevelFinish非常简单。finishedLevel也非常简单,你所面对的唯一复杂元素便是在一开始销毁所有的VRope对象,然后所有对象都与之联系在一起,除了地面实体。这便是你在割断绳子并留下糖果时所创造的所有对象。

你正在使用一个设置而不是一个矢量,因为当在所有的绳索中循环时,糖果将被附加到更多绳索上。在这种情况下,如果你使用一个矢量,你便能够添加同样的实体两次并在销毁循环中将其删除两次。一个设置能够通过避免添加同样的对象两次而解决这一情况。

在销毁循环中,你也必须销毁实体的相关精灵。

现在你只需要在VRope中呈现节点实例变量,你便算完成设置了!

打开VRope.h并在cutRopeInStick:newBodyA:newBodyB:的声明之后添加如下内容:

@property (nonatomic, readonly) b2RopeJoint *joint;

然后添加如下内容到VRope.mm,也就是在#ifdefBOX2D_H后:

@synthesize joint = joint;

酷!运行项目并游戏!

添加更多糖果

这已经很有趣了,不是吗?在这一教程中你已经取得很大进展了!你可以添加一些最后的碰触进行更好地完善。

前往HelloWorldLayer.mm的initLevel并在向前移动世界的组块前添加如下内容:

// Add the candy
b2Body *body2 = [self createCandyAt:CGPointMake(s.width * 0.5, s.height)];

// Change the linear dumping so it swings more
body2->SetLinearDamping(0.01);

// Add a bunch of ropes
[self createRopeWithBodyA:groundBody anchorA:cc_to_b2Vec(s.width * 0.65, s.height + 5)
bodyB:body2 anchorB:body2->GetLocalCenter()
sag:1.0];

创建并运行,你将看到上方悬挂了更多的糖果。

另外一个能够轻松完善的内容便是绳索的质量。你也许注意到,当绳索有点不自然时,你便能够看到树枝。为了解决这种情况,你需要改变VRope的其中一个参数。

打开VRope.mm并前往createRope:pointB:distance:找到这行内容:

int segmentFactor = 12; //increase value to have less segments per rope, decrease to have more segments

如果你将数值减少到6,你的绳索便会有更多VStick段数,并将变得更加自然。当然,你减少的数值越大,绳索模拟便会越长。所以这里存在一个性能权衡—-通过测试去观察怎样才是最适合你的应用。

另外一个奇怪的内容便是绳索摇摆的幅度太大且有点不稳定,甚至在保持静态一会后还会摇摆。你也可以修改这一问题。打开VPoint.m并找到applyGravity::

-(void)applyGravity:(float)dt {
y -= 10.0f*dt; //gravity magic number
}

如果你减少了这一数值(10,0),绳索将变得“更轻”,并能够更自然地移动。尝试着将其改为1.0。

仍会发生的一件事便是菠萝和世界上缘间的碰触看起来有点奇怪—-菠萝竟然会与天空碰撞在一起?!所以删除上缘,连同左右边缘,并稍微提高底缘去拓宽世界。

打开HelloWorldLayer.mm并前往initPhysics。删除这些内容:

// top
groundBox.Set(b2Vec2(0,s.height/PTM_RATIO), b2Vec2(s.width/PTM_RATIO,s.height/PTM_RATIO));
groundBody->CreateFixture(&groundBox,0);

// left
groundBox.Set(b2Vec2(0,s.height/PTM_RATIO), b2Vec2(0,0));
groundBody->CreateFixture(&groundBox,0);

// right
groundBox.Set(b2Vec2(s.width/PTM_RATIO,s.height/PTM_RATIO), b2Vec2(s.width/PTM_RATIO,0));
groundBody->CreateFixture(&groundBox,0);

然后将这一行:

groundBox.Set(b2Vec2(0,0), b2Vec2(s.width/PTM_RATIO,0));

改为:

groundBox.Set(b2Vec2(-s.width/PTM_RATIO,0), b2Vec2(2*s.width/PTM_RATIO,0));

最后,一些内存管理附加到VRope上。如果你在项目中使用Xcode的Analyze,你将看到一些带有内存管理的潜在问题:不寻常的执行方法将会演变成问题所在。实际上,它们可能已经成为一种问题了,因为你释放了在cutRopeInStick:bodyA:bodyB:中所创造的VPoint,并且它在dealloc方法中再次释放了它。

为了让类紧跟着最佳实践,打开VRope.mm,找到createRope:pointB:distance:并改变它去添加两个释放声明,如下:

for(int i=0;i&lt;numPoints;i++) {
CGPoint tmpVector = ccpAdd(pointA, ccpMult(ccpNormalize(diffVector),multiplier*i*(1-antiSagHack)));
VPoint *tmpPoint = [[VPoint alloc] init];
[tmpPoint setPos:tmpVector.x y:tmpVector.y];
[vPoints addObject:tmpPoint];

// Add this line:
[tmpPoint release];
}
for(int i=0;i&lt;numPoints-1;i++) {
VStick *tmpStick = [[VStick alloc] initWith:[vPoints objectAtIndex:i] pointb:[vPoints objectAtIndex:i+1]];
[vSticks addObject:tmpStick];

// Add this line:
[tmpStick release];
}

现在前往dealloc并删除这些内容:

for(int i=0; i < numPoints; i++) {
[[vPoints objectAtIndex:i] release];
if(i!=numPoints-1)
[[vSticks objectAtIndex:i] release];
}

现在便算解决了内存问题!

无偿的音乐和音效

游戏很酷,但却与我的想法有点出入。添加一些声音和背景音乐去活跃它!

我从incompetech.com中选择了一首很棒的丛林歌曲,并从freesound.rog中选择了一些音效。你也可以做出自己的选择。

提取档案文件并将其拖到你的项目的资源文件夹中:

sounds(from raywenderlich)

sounds(from raywenderlich)

确保选中“Copy items into destination group’s folder”去复制这些文件到你的项目文件夹中。

copy(from raywenderlich)

copy(from raywenderlich)

然后添加如下输入和常量到你的HelloWorldLayer.mm的最上方:

#import “SimpleAudioEngine.h”

#define kCuttingSound       @”cut.caf”
#define kBiteSound          @”bite.caf”
#define kSplashSound        @”splash.caf”
#define kBackgroundMusic    @”CheeZeeJungle.caf”

现在添加这一代码到init,即在scheduleUpdate调用前面:

[[SimpleAudioEngine sharedEngine] preloadEffect:kCuttingSound];
[[SimpleAudioEngine sharedEngine] preloadEffect:kBiteSound];
[[SimpleAudioEngine sharedEngine] preloadEffect:kSplashSound];
[[SimpleAudioEngine sharedEngine] playBackgroundMusic:kBackgroundMusic loop:YES];
[SimpleAudioEngine sharedEngine].backgroundMusicVolume = 0.4;

这预先加载了你的所有音频文件。它同样也播放了背景音乐并在其结束时将其设置为自动循环。最后一行内容稍微降低了背景音乐的音量,所以它并不会压过其它声音。

现在你只需要在一些关键的地方添加音效便可。

当用户割断绳索时必须出现割断的声音。你应该记得,这发生在ccTouchesMoved:,即在两个循环的“if”条件中。找到ccTouchedMoved:withEvent:并在返回声明(在“if”条件中)前添加如下代码:

[[SimpleAudioEngine sharedEngine] playEffect:kCuttingSound];

咬声应该出现在update之后,即在最后的if条件(你要求鳄鱼闭上嘴巴)中。在if中添加如下内容,即在changeCrocAttitude调用之后:

// Play sound effect
[[SimpleAudioEngine sharedEngine] playEffect:kBiteSound];

最后但同样重要的是菠萝掉落水里的飞溅声音。这也发生在update中。记得你是何时添加代码让菠萝掉落的吗?在这里应该触发音效。在update中找到if条件并添加如下内容到if条件中(在最上方):

[[SimpleAudioEngine sharedEngine] playEffect:kSplashSound];

运行并测试。只需要几行代码便能够创造出改变游戏的音效了!

知道何时结束

该结束的时候游戏便会结束,但是你应该呈现一些文本让用户知道他们是否喂到了鳄鱼并获取胜利,或者让鳄鱼失望而遭遇失败。

crocdile(from raywenderlich)

crocdile(from raywenderlich)

首先,存在一种情况是当糖果掉落到水里时,用户遭遇失败。在update中添加飞溅声音的同样位置上添加代码,即在飞溅声音调用之后:

CGSize s = [[CCDirector sharedDirector] winSize];
CCLabelTTF *loseLabel = [[CCLabelTTF alloc] initWithString:@”Try Again!”
dimensions:s
hAlignment:kCCTextAlignmentCenter
vAlignment:kCCVerticalTextAlignmentCenter
fontName:@”Verdana-Bold”
fontSize:60.0];
loseLabel.color = ccc3(255, 0, 0);
loseLabel.anchorPoint = CGPointZero;
[self addChild:loseLabel];

这添加了一个红色标签到屏幕的中心。

为了删除信息,在完成组块中添加一个调用去删除它:

// Remove the sprite and check if should finish the level.
CCFiniteTimeAction *finish = [CCCallBlockN actionWithBlock:^(CCNode *node)
{
[self removeChild:node cleanup:YES];
[self checkLevelFinish:YES];
// add this line:
[self removeChild:loseLabel cleanup:YES];
}];

如果用户因为将所有糖果都喂给了鳄鱼而获得胜利,那么通过在checkLevelFinish中的“if”条件最后添加这一内容而告诉他们:

if ([candies count] == 0 && !forceFinish)
{
CGSize s = [[CCDirector sharedDirector] winSize];
CCLabelTTF *winLabel = [[CCLabelTTF alloc] initWithString:@”Level Finished!”
dimensions:s
hAlignment:kCCTextAlignmentCenter
vAlignment:kCCVerticalTextAlignmentCenter
fontName:@”Verdana-Bold”
fontSize:60.0];
winLabel.color = ccc3(255, 0, 0);
winLabel.anchorPoint = CGPointZero;
[self addChild:winLabel];
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
[self removeChild:winLabel cleanup:YES];
});
}

运行并尝试着让鳄鱼吃掉两个糖果!上面的糖果真的很难处理!有时候鳄鱼真的要让我抓狂了!

最后

整个过程真的是既困难又有趣!现在你可以继续完善这款游戏。我敢保证你能够找到许多方法去完善它。

本文为游戏邦/gamerboom.com编译,拒绝任何不保留版权的转载,如需转载请联系:游戏邦

How to Make a Game Like Cut the Rope – Part 2

Welcome back to our How to Make a Game Like Cut the Rope tutorial series!

In the first part of the series, you created the game’s basic scene and used Box2D’s b2RopeJoint and PatrickC’s VRope class to build a rope.

In this second and final part of the series, you’ll get to the best part – actually cuting that rope! :]

Getting Started: Cut the Verlet!
To cut the rope, you need to keep track of the user’s touches on screen and check if a touch intersects with a rope.

When the user drags his or her finger on the screen, the ccTouchesMoved:withEvent: method is called by Cocos2D, and you can use it to check the finger’s current position and last tracked position. Then you can check if the line formed by these two points intersects with any of the ropes on your scene.

To know which rope to cut and where, it helps to understand a bit more about how VRope works.

A VRope is composed of a series of VPoint objects that are connected using VStick objects. So, if you want to cut a VRope, the best way is to figure out if the line formed by the user’s finger movement intersects any of the VStick objects of any of the VRopes in your scene.

The first thing to do is to find a good algorithm to check if two lines intersect. The best one I found is explained very well on this page. You’ll implement this algorithm as a method in your HelloWorldLayer class.

Go ahead and add this to the end of HelloWorldLayer.mm:

This method is an implementation of the equations from the above site, simplified for this project. Since you don’t care much about the intersection point, only whether or not the lines intersect, you don’t need to calculate x and y. You only need to check if ua and ub are within 0 and 1.

You’re almost ready to implement the ccTouchesMoved:withEvent: method. Before you do, expose a few instance variables of VRope and VPoint that you’ll need.

First you need to expose the sticks array of VRope. Open VRope.h and add this inside the interface block:

Then synthesize the property in VRope.mm inside the implementation block:

Now you need to expose the coordinates of the VPoint class. Add this to VPoint.h:

And this to the end of VPoint.m:

You’re almost ready now. Add the skeleton of ccTouchesMoved:withEvent: to the end of HelloWorldLayer.mm:

This is pretty simple. For every rope, you check each of the sticks used to create the rope. As soon as you find a stick that intersects the line dragged by the user, you cut the rope and stop the loop.

OK, so not that simple, as you still need to figure out how to cut the rope! :P

Let’s think about this for a few moments before you rush off to VRope again to add this functionality. A VRope is backed by a b2RopeJoint, and this rope joint in turn needs a body at each end. When you cut a VRope, you want to split it into two VRope objects (or create a new one), and for this you need another b2RopeJoint and two new bodies.

What? Two new bodies? Thinking in real world terms, this seems weird. When you cut a rope, two bodies do not suddenly come into existence.

That may be true, but in your little Box2D world, your rope is not really a rope! It’s just a logical binding between two bodies. So when you cut the rope in your world, to keep the illusion alive you’ll attach a very small, invisible object to each end of the cut, so the ropes maintain their weight and rope-like behavior.

Here’s a rough illustration of what you’re going to implement:

OK, it’s not a masterpiece of art, but you get the idea. :] The code and explanation below will make this even clearer.

On to the implementation then!

Go to VRope.h and add this method declaration right after -(void)reset:

You’ll feed this method two new bodies and the VStick where you want to cut the rope. Now let’s see how it’s done. Open VRope.mm and add this method right after reset:

Wow, that’s a big one! I’ll try to explain everything done by the above code with the help of another masterpiece of illustration:

The illustration shows the VPoints and VSticks that make up the VRope. As you can see, the VRope has seven points and, therefore, six sticks. The indices of the points are indicated above them and the indices of the sticks under them.

In this example, the rope will be cut in the middle (nPoint = 3). The idea is to use the original VRope’s data so that the cut appears natural.

Let’s go though the code above, following the numbered comments:

1.First determine the point at which the cut will be made. The passed stick object will be the first stick belonging to the new rope, so its index is the index of the point where the cut will happen. In the illustration above, this is 3.

2.Define a range struct to indicate where you want your vSticks array to split. In the illustration, this range will have location=3 and length=3. This indicates the new rope will get sticks from 3 to 5.

3.Create a new array with sticks that will go to the new rope.

4.Remove these sticks from the current rope’s array.

5.Do the same for the sprites that are used to draw the sticks.

6.Extend the length of the range by one, because the number of points is always the number of sticks + 1. Create a new array with the points that will be in the new rope (points 3 to 6 in the illustration) and remove them from this object’s array.

7.You probably noticed in the illustration that the breaking point of the rope was removed from this object’s array and will go to the new rope. To rectify this you need to “clone” this point and add it to this object’s points array. The last stick of this object also needs to be updated to point to the cloned end point.

8.The cut ratio will be used to determine the length of the new ropes. In the illustration, this ratio will be 0.5.

9.Fix the number of points of this object. In the illustration, it becomes 4.

10.Recreate the joint that will be associated with this object. Attach bodyA and the newBodyB to this rope joint and use the old length and cut ratio to determine the new length. Also, place the newBodyB where the rope was cut.

11.Create the new rope joint and attach the newBodyA and bodyB to it, also placing the newBodyA in the position of the cut.

12.Then destroy the old joint and update the instance variable to the new joint.

13.And finally, create the new VRope object using the arrays you collected before and the new joint. You still need to implement this init method, but it’s very simple and the code’s coming up next.

As promised, here’s the init method. Add this below the code you just added:

Yes, it’s pretty simple. It just keeps track of the arrays you passed in above and updates the numPoints instance variable.

You’ve probably noticed that you have a warning in your code about a method called setPointB not being found. Yes, I slipped it by you! I was hoping not to complicate things further while explaining the massive rope-cutting method. But this is very easy to fix.

Open VStick.h and add this declaration:

Then open VStick.m and add this at the end:

As you can see, just a simple setter method. Now your program should compile without any issues, and you can finally cut the rope!

Except… before you cut the rope, you need two new bodies. In HelloWorldLayer.mm, add the following new method to create these “tip” bodies, as I call them:

The only thing worth pointing out here is the maskBits property. It’s set to zero so that it won’t collide with anything else in your world, and that’s how it should be since this is just a body you’re using to simulate the rope’s weight.

Now, go back to ccTouchesMove:withEvent: and add these lines before the return statement (where it says “Cut the rope here”):

This applies everything you created above and cuts the selected rope at the correct point.

That’s enough code for now. Build and run, and you should be able to cut a rope with your finger (or mouse if in the simulator…):

If you feel as if you need a break, don’t worry, the fun and games are just beginning (pun intended). :]

The repository tag for this point in the tutorial is RopeCutting.

A Pineapple-eating Crocodile? Why Not?
Now that the hard part is over, let’s add some gameplay and feed that pineapple to the crocodile.

You already have another sprite of the croc with its mouth open in your sprite sheet. What you’re going to do is make the croc open and close it’s mouth from time to time. The player will have to time the rope cuts so as to drop the pineapple over the croc’s mouth when it’s open. The croc decides when he’s hungry!

You need a way to check if the pineapple, as it drops, makes contact with the croc’s mouth. To do this, you’ll use Box2D’s contact listeners. You should be familiar with contact listeners from previous tutorials. If not, take a look at How To Create A Breakout Game with Box2D and Cocos2D – Part 2.

The first step is to add a body that will simulate the croc’s mouth, so the contact listener will have something to check for contact with the pineapple. Open HelloWorldLayer.h and add these lines:

Then go to initPhysics in HelloWorldLayer.mm and add this at the end:

You first create a static body with the origin on the bottom left corner of your sprite. You then create a fixture to simulate the croc’s mouth bottom (the one you’ll use in your collision detection). If you run the project, you can see the fixture in the right position:

You’ll be able to see it better once you open the croc’s mouth. You might wonder why the body (the physics body for the croc’s mouth, not it’s actual body :]) is set to be inactive (last line). This is because the croc’s mouth is initially closed. In that state, you don’t want the pineapple to interact with it.

Time to make the croc open its mouth. Add these two variables to HelloWorldLayer.h:

Now add this method to HelloWorldLayer.mm:

This method changes the boolean that indicates if the croc has its mouth open or not, changes the sprite to reflect this, sets the body to active if the mouth is open, and finally, starts a timer that will repeat this after a random amount of time between 3 and 5 seconds.

Notice that you’re also changing the croc’s sprite zOrder. This achieves the following behavior: When the croc’s mouth is open, if the pineapple drops, it will fall behind the croc (because the pineapple now has a zOrder of 0 and the croc a zOrder of 1), giving the illusion of having been eaten. If the croc’s mouth is closed, then the opposite happens and the pineapple falls in front of the croc. You’ll see this in action in a moment.

Before you forget, add this to dealloc in HelloWorldLayer.mm:

And finally, make the initial call to changeCrocAttitude at the end of initLevel:

After the above call, changeCrocAttitude will itself take care of calling the method again at random intervals.

Great! Build and run the project and you should see the croc’s mouth open and close from time to time:

If you drop the pineapple in the croc’s mouth when it’s open, it will lay there for a while, but when the croc’s mouth closes, the pineapple drops into view. Don’t worry, you’ll fix this in a moment with a contact listener.

The repository tag for this point in the tutorial is CrocAttitude.

You Have Contact!

For the contact listener, you’ll mostly use code from the breakout game tutorial mentioned above. So begin by downloading the contact listener developed in that tutorial. Here are the include and implementation files.

Drag these files to your project tree. Be sure to check the “Copy items into destination group’s folder” to copy these files to your project folder.

Now, open HelloWorldLayer.h and add this include:

And this to the class variables:

Open HelloWorldLayer.mm and add these lines at the end of initPhysics:

Add the following cleanup code to dealloc:

Now add this code to the end of update (don’t worry about the two warnings and an error that will appear, as you’ll fix those real quick):

This looks complicated, but it’s not, it’s just long. It’s a variation of the code used in the breakout game tutorial.

First you iterate through all the contacts. If any contact involves either the floor or the croc’s mouth, you keep a reference to the other body, as it might be a candy (in this project, it can’t be any other body, as the rope does not interact with either the body or the ground, but it’s better to include this check now, in case you add other elements to the game play later on). You then check the candies array, and if the potential candy body is there you can be sure it’s a candy.

If it is indeed a candy, you add it to the destruction vector. The difference is that, if it hit the croc’s mouth, you set a bool to true to make the croc’s mouth close. But if it hit the floor, then the croc’s mouth doesn’t need to close.

In the case of the candy hitting the floor, you’re going to add an animation to the sprite to simulate the candy sinking. At the end of the animation, you’ll force the level to end, since in this game you can only pass the level if the croc eats all the candy. (You’ll add this method shortly.)

After this first loop checks all the contacts, you go through the toDestroy vector to remove the physics bodies that need to be destroyed. One complication in your game is the removal of the VRope objects that might be associated with the destroyed object. So, for every destroyed object, you go though its joints and check if they belong to any VRope object. If they do, you remove the sprites from the scene and the VRope object from your ropes array.

Finally, you close the croc’s mouth if you had any mouth hits, and call a method (not yet written) to check if the level should end.

Now you should have two warnings about the checkLevelFinish method, and one error because the VRope object is not exposing the joint instance variable. Fix these problems now.

You’ll use another standard C++ class, so add this to the includes of HelloWorldLayer.mm:

Now add checkLevelFinish to HelloWorldLayer.mm:

I slipped the implementation for finishedLevel in there too. :]

checkLevelFinish is simple enough. finishedLevel is also pretty simple, the only complication being that you have to destroy all the VRope objects first, and then all the objects that were associated with them, except for the ground body. These are all the “tip” objects you created when cutting the ropes and the remaining candies that might still exist.

You’re using a set instead of a vector because, when looping through all the ropes, a candy can be attached to more than one rope. In this case, if you used a vector you would add the same body twice and remove it twice in the destroy loop. A set solves this by not adding the same object twice.

In the destroy loop, you also have to destroy the body’s associated sprite, again, in case it’s a candy.

Now you only need to expose the joint instance variable in VRope, and you’re done!

Open VRope.h and add this after the declaration of cutRopeInStick:newBodyA:newBodyB::

Then add this to VRope.mm right after the #ifdef BOX2D_H line:

Cool! Run the project and play!

The repository tag for this point in the tutorial is GameOver.

Adding More Candy
This has been fun, hasn’t it? You’ve come a long way in this tutorial! You can add some final touches to make it even better.

Go to initLevel in HelloWorldLayer.mm and add this block right before the block that moves the world forward a bit:

Build and run, and you’ll see that this adds one more candy swinging from the top.

Another thing that can be easily improved is the quality of the rope. You may have noticed that sometimes the rope is a little unnatural and you can see the sticks. To improve this, you need to change one of the parameters of VRope.

Open VRope.mm and go to createRope:pointB:distance: and find this line:

If you decrease this number to, say, 6, your rope will have more VStick segments and will behave more naturally. Of course, the more you decrease it the longer it takes to perform the rope simulation, so there’s a performance tradeoff – test to see what works best for your app.

Another weird thing that happens is that the rope swings a lot and is a little unstable, swinging even after it’s been static for a while. You can tweak this too. Open VPoint.m and find applyGravity::

If you decrease this number (10.0), the rope will become “lighter” and move more naturally. Try changing it to 1.0.

One thing that might still happen is a contact between the pineapple and the top edge of the world, which looks kinda weird – the pineapple colliding with the sky! So remove the top edge, along with the left and right edges, and increase the bottom edge a bit to widen your world. :]

Open HelloWorldLayer.mm and go to initPhysics. Remove these lines:

And change this line:

To:

Finally, some memory management fixes to VRope. If you use Xcode’s Analyze on the project, you’ll see some potential problems with memory management: unusual ways of doing things that could become a problem. In fact, they actually have become a problem, since you’re releasing the VPoint created in cutRopeInStick:bodyA:bodyB:, and it releases it again in the dealloc method.

To make the class follow best practices, open VRope.mm, find createRope:pointB:distance: and change it to add the two release statements (indicated via comments) below:

Now go to dealloc and remove these lines:

And that’s it, memory problems solved and analyzer happy!

The repository tag for this point in the tutorial is MinorTweaks.

Gratuitous Music and Sound Effects
The game is pretty cool as it is, but it’s a little quiet for my tastes. Add some sounds and background music to liven it up. :]

I’ve selected a nice jungle song from incompetech.com and some sound effects from freesound.org. You can select your own, or get the ones I picked out here.

Extract the archive and drag it to the Resources folder of your project:

Be sure to check the “Copy items into destination group’s folder” to copy these files to your project folder.

Then, add the following import and constants to the top of your HelloWorldLayer.mm:

Now, add this code to init, just before the scheduleUpdate call:

This preloads all your effect audio files. It also starts playing the background music and sets it to automatically loop when it ends. The last line lowers the volume of the background music a bit, so that it doesn’t drown out the other sounds.

Now you just need to add the effects at some key places.

The cutting sound should play when the user cuts the rope. As you remember, this happens in ccTouchesMoved: in an “if” condition inside two for loops. Find ccTouchedMoved:withEvent: and add this right before the return statement, inside the “if” condition:

The bite sound should go at the end of update, inside the last if condition where you force the croc’s mouth to close. Add this inside the if, right after the call to changeCrocAttitude:

Last but not least is the splash sound for the pineapple falling into the water. This also happens inside update. Remember when you added the code to make the pineapple sink? The sound effect should be triggered there. Find if (hitTheFloor) inside update and add this inside the if condition (at the top):

And that’s it! Run and test it out. It’s amazing how a few lines of code and some sound effects can change a game!

Knowing When Its Over

It’s over when it’s over, but you should display some text to let the user know if they’ve fed the crocodile and won, or disappointed the crocodile and lost.

First, there’s the case of when the user lost because a piece of candy fell in the water. Add the code in the same location that you added the splash sound, in update, right after the splash sound call:

This adds a label to the center of the screen, in red.

To make the message go away, add a call to remove it just a few lines below, inside the finish block (just add the line indicated with a comment, not the whole block):

If the user won by feeding all the candy to the crocodile, let them know by adding this at the end of the “if” condition inside checkLevelFinish (after the other existing code in the condition):

That’s it! Run and try to make the croc eat the two candies! That candy at the top is tricky, isn’t it? The croc really makes me mad sometimes….

The repository tag for this point in the tutorial is FinalProject.

Where to Go From Here?

Well, that was fun! Hard, but fun! Now go and make this game even better. I’m sure that are lots of ways to improve it. So, fork the project on github, make it better and add a pull request. I’d love to see what you’re capable of!(source:raywenderlich)


上一篇:

下一篇: