第一部分木有多少东西,所以不翻译了。想看en的可以去这个网址:
http://www.raywenderlich.com/7261/monkey-jump?utm_source=feedburner&utm_medium=feed&utm_campaign=Feed%3A+RayWenderlich+%28Ray+Wenderlich+%7C+iPhone+Developer+and+Gamer%29&utm_content=Google+Reader
第二章
增加一个monkey类,继承自GB2Sprite。记住将.m的扩展名变成.mm。这个monkey会在这个游戏的世界里做很多事情:“monkey在有东西从上方降落的时候,举物品的时候,跳跃的时候等等,都会举起双手。所以,你就需要一些变量来存储这些额外的数据。
将如下的代码替换掉原来monkey.h里面的代码。
#pragma once
#import "Cocos2d.h"
#import "GB2Sprite.h"
@class GameLayer;
@interface Monkey : GB2Sprite
{
float direction; // 保持猴子的方向 (通过加速计)
int animPhase; // 当前动画相位
ccTime animDelay; // 延迟至下一个动画出现
GameLayer *gameLayer; // 用来指向游戏layer
}
-(id) initWithGameLayer:(GameLayer*)gl;
-(void) walk:(float)direction;
@end
现在将如下代码替换掉monkey.mm:
#import "Monkey.h"
#import "GB2Contact.h"
#import "GMath.h"
#import "Object.h"
#import "SimpleAudioEngine.h"
#import "GameLayer.h"
#define JUMP_IMPULSE 6.0f
#define WALK_FACTOR 3.0f
#define MAX_WALK_IMPULSE 0.2f
#define ANIM_SPEED 0.3f
#define MAX_VX 2.0f
@implementation Monkey
-(id) initWithGameLayer:(GameLayer*)gl
{
// 1 - 初始化猴子
self = [super initWithDynamicBody:@"monkey" spriteFrameName:@"monkey/idle/1.png"];
if(self)
{
// 2 - 不让猴子歪倒
[self setFixedRotation:true];
// 3 - The monkey uses continuous collision detection
// to avoid getting stuck inside fast-falling objects
[self setBullet:YES];
// 4 - 指向游戏layer
gameLayer = gl;
}
return self;
}
@end
让我们一步步的看这个initWithGameLayer方法:
1.首先初始化这个monkey。这个猴子的动作会被物理引擎所影响,因此让它初始化成为动态对象(dynamic object),我们将会使这个闲置的框架成为monkey动作和物理模型的第一个框架。
2.这个猴子应该一直站着,因此我们也设置了它的倾斜属性(rotation)。这意味着monkey被box2d驱动,但不会歪倒或者倾斜。
3.将这个monkey设置成bullet模式。bullet模式支持对一个对象的连续碰撞测试。不用bullet模式,box2d只是移动对象,然后完成碰撞检查。如果是快速移动的物体,其中一个对象可能穿过或者卡在另一个对象。连续碰撞测试从一个对象的当前位置到另一个新的位置期间会一直计算──并不只是终点。
4.最后我们需要将gamelayer指针指过来。让它保持弱引用,仅仅assign这个值。
如果你的项目里只有很少一些对象,你可以对所有的游戏对象进行box2d引擎连续碰撞检测,然而当一个游戏有很多对象的时候,这样做只会增加cpu的负荷。因此,对于我们这个游戏,我们只对monkey和快速移动或降落的对象设置为bullet模式。
打开GameLayer.h,将 @class Monkey;写在最上面,现在在GameLayer类里面写上Monkey *monkey;然后到GameLayer.mm,在最上写上#import "Monkey.h" ,在GameLayer.mm里面的init方法最后初始化monkey,将它加到游戏layer上设置一个初始的位置:
monkey = [[[Monkey alloc] initWithGameLayer:self] autorelease];
[objectLayer addChild:[monkey ccNode] z:10000];
[monkey setPhysicsPosition:b2Vec2FromCC(240,150)];
现在就可以运行一下,看看效果。
接下来我们要让monkey走起来。使用加速计作为数据源。
返回GameLayer.mm,然后将 self.isAccelerometerEnabled=YES;加入到init方法的最后。
这将会确保每一点加速计值的变动都会被通知到GameLayer类里,这个通知者已经被写在了GameLayer.mm文件的最后,在@end之前:
-(void)accelerometer:(UIAccelerometer*)accelerometer
didAccelerate:(UIAcceleration*)acceleration { // forward accelerometer value to monkey [monkey walk:acceleration.y]; } |
这个通知者会随着y轴值的变化而调用monkey的walk方法。这个方法将会根据加速计的值而让monkey来来回会移动。
现在我们再转到Monkey.mm,在@end前加上walk方法,这个方法可以将最新的动作方向传给全局变量。
-(void) walk:(float)newDirection
{
direction = newDirection;
}
现在运行一下,额,木有反应?当然,还木有将变量与动作联系起来。
将下面代码添加到Monkey.mm:
-(void) updateCCFromPhysics { // 1- Call the super class [super updateCCFromPhysics];
// 2 - Apply the directional impulse float impulse = clamp(-[self mass]*direction*WALK_FACTOR, -MAX_WALK_IMPULSE, MAX_WALK_IMPULSE); [self applyLinearImpulse:-b2Vec2(impulse,0) point:[self worldCenter]]; } |
最好不要完全控制monkey,,否则像它的降落和碰撞测试不会正常工作了。
你真正做的是给这个monkey一个正确方向的推动。因为物理引擎会每秒钟更新60次,保持推的力度要轻很重要。这是个monkey,不是个bullet,虽然这个monkey是个bullet模式。
可以用推动力来移动这个box2d对象。你还可以使用GB2Sprite的applyLinearImpulse方法,有两个参数:推动力和作用点。
对于这个作用点,我们用的是这个对象的质心,作用于这个对象的质心上,可以防止这个物体旋转。
当请求推动力的时候,我推荐使用物体的质量,这个你可以通过[self mass];来获取。因为推动力是质量和速度的产物。
用已经得到的方向值乘以对象的质量。这将会得到一个值,随着倾斜的角度的增加而增加。
根据质量来得出推动力使我们不用担心在physicseditor里面修改对象形状的时候担心物体运动的变化了。
如果我们不这样做的话,一会我们将monkey变小了,同样的推动力会将这个更小的monkey的反应更加剧烈。
我们也会控制这个值不会太大,设置了一个最大值MAX_WALK_IMPULSE。
现在我们就必须要在真机上运行了。额,现在猴子可以左右滑动了,但是动作显得不那么自然。
我们现在将下面的代码添加到Monkey.mm的updateCCFromPhysics方法的最后。
animDelay -= 1.0f/60.0f; if(animDelay <=0) { animDelay = ANIM_SPEED; animPhase++; if(animPhase >2) { animPhase = 1; } } |
第一行我设置了一个时间变量,来减去时间1/60s,因为系统并木有提供一个递减的参数来供引用,之所以设置1/60s,是因为每秒这个方法会更新60次。
如果animDelay降到0,重新设置animDelay值成animation speed,并且将current phase加一,如果最高的phase达到了,然后重新设置为1,然后循环。
接下来我们要确定monkey面对的方向,有两个方法来实现:
1.使用加速计的方向
2.使用monkey的速度矢量
我比较倾向于使用加速计。因为它能更快的对试图通过倾斜机器来改变方向做出反应。将下面代码添加到updateCCFromPhysics方法的最后:
// determine direction of the monkey
bool isLeft = (direction < 0);
// direction as string
NSString *dir= isLeft ? @"left": @"right";
// update animation phase
NSString *frameName;
const float standingLimit= 0.1;
float vX =[self linearVelocity].x;
if((vX >-standingLimit)&& (vX < standingLimit))
{
// standing
frameName = [NSString stringWithFormat:@"monkey/idle/2.png"];
}
else
{
// walking
NSString *action= @"walk";
frameName = [NSString stringWithFormat:@"monkey/%@/%@_%d.png", action, dir, animPhase];
}
// set the display frame
[self setDisplayFrameNamed:frameName];
所有上面的代码达到的效果是,如果monkey的速度小于standingLimit,它在玩家看来就是站着的,否则,它将会调用walk的框架来实现当前的位置和动作。
我依然感到不太高兴的是这个猴子移动的太快了,我们可以通过减小impulse值,但是这个会使他显得很慢而且笨拙。
我们需要一个足够强的impulse来让他反应快点,但是不要太快。将下面的代码替换掉Monkey.mm的updateCCFromPhysics方法里的当前代码:
// 1 - Call the super class
[super updateCCFromPhysics];
// 2 - Update animation phase
animDelay -= 1.0f/60.0f;
if(animDelay <=0)
{
animDelay = ANIM_SPEED;
animPhase++;
if(animPhase >2)
{
animPhase = 1;
}
}
// 3 - Get the current velocity
b2Vec2 velocity = [self linearVelocity];
float vX = velocity.x;
// 4 - Determine direction of the monkey
bool isLeft = (direction < 0);
if((isLeft&& (vX > -MAX_VX)) || ((!isLeft && (vX < MAX_VX))))
{
// apply the directional impulse
float impulse = clamp(-[self mass]*direction*WALK_FACTOR,
-MAX_WALK_IMPULSE,
MAX_WALK_IMPULSE);
[self applyLinearImpulse:-b2Vec2(impulse,0) point:[self worldCenter]];
}
// 5 - Get direction as string
NSString *dir= isLeft ? @"left": @"right";
// 6 - Update animation phase
NSString *frameName;
const float standingLimit= 0.1;
if((vX >-standingLimit)&& (vX < standingLimit))
{
// standing
frameName = [NSString stringWithFormat:@"monkey/idle/2.png"];
}
else
{
// walking
NSString *action= @"walk";
frameName = [NSString stringWithFormat:@"monkey/%@/%@_%d.png", action, dir, animPhase];
}
// 7 - Set the display frame
[self setDisplayFrameNamed:frameName];
正如你注意到的,我们去掉了一些代码块,但是主要的变化还是#3对vX变量的设置,而且把impulse的代码放在了#4,并且包装在了if结构里面,这是用来判断当前位置速率是否小于最大值,这能保证monkey推着一个物体运动,并且使自己不会加速过快。
现在运行一下。
现在我们让monkey跳。所以我们必须让游戏检测到在gamelayer上的点击事件,因为我们想在每点击一次,monkey就会jump,打开GameLayer.mm,在init方法中添加下面一句代码:
self.isTouchEnabled=YES;
当然我们还要将下面的这个方法添加到@end之前:
-(void)ccTouchesBegan:(NSSet*)touches withEvent:(UIEvent*)event
{
[monkey jump];
}
你可能注意到了在Monkey.mm里面并木有jump方法,所以我们要转换到Monkey.h,在@end前面加上下面一句:
-(void) jump;
p现在我们打开Monkey.mm并且把下面的代码添加到@end之前:
-(void) jump { [self applyLinearImpulse:b2Vec2(0,[self mass]*JUMP_IMPULSE) point:[self worldCenter]];
[[SimpleAudioEngine sharedEngine] playEffect:@"jump.caf" pitch:gFloatRand(0.8,1.2) pan:(self.ccNode.position.x-240.0f)/ 240.0f gain:1.0]; } |
所有上面的代码实现的,使monkey跳,并且播放jump音乐。
但是这远远不够,如果你多次点击屏幕以后,monkey将会跳出屏幕外!更糟糕的是摄像头木有跟着动。
将下面一段代码添加到GameLayer.mm的update方法的最上面:
float mY =[monkey physicsPosition].y* PTM_RATIO;
将下面的代码添加到#7下面:
// 8 - Adjust camera const float monkeyHeight= 70.0f; const float screenHeight= 320.0f; float cY = mY- monkeyHeight - screenHeight/2.0f; if(cY <0) { cY = 0; } |
// 9 - Do some parallax scrolling [objectLayer setPosition:ccp(0,-cY)]; [floorBackground setPosition:ccp(0,-cY*0.8)];// move floor background slower [background setPosition:ccp(0,-cY*0.6)]; // move main background even slower |
你玩一会,你会发现物品会从monkey的当前位置掉落,实际上,过些时间,物品将不会掉落,因为他们已经堆积的超过了降落点了。为了解决这个问题,我们将GameLayer.mm的update方法中的 float yPos=400; 替换成 float yPos=400+mY;
现在物品将会一直在monkey的头上方400的位置降落,不管它在哪里。返回monkey,你会发现monkey会在空中一直跳,这是不允许的,我们需要它在接触floor的时候才能跳。让我们来计算monkey触碰floor的次数。在Monkey.h中添加下面这个变量: int numFloorContacts;
转换到Monkey.mm,将下面这段代码添加到@end之前:
-(void) beginContactWithFloor:(GB2Contact*)contact { numFloorContacts++; }
-(void) endContactWithFloor:(GB2Contact*)contact { numFloorContacts--; } |
以上两个方法将会在monkey和floor触摸开始和结束的时候被GBox2D调用。
如果monkey站在floor上,那么numFloorContacts的值将至少为1.我们将用下面这段if结构的代码来使monkey在接触到floor的时候才能jump:
-(void) jump { if(numFloorContacts >0) { [self applyLinearImpulse:b2Vec2(0,[self mass]*JUMP_IMPULSE) point:[self worldCenter]];
... } } |
运行一下,一切貌似都正常,但是monkey站在一个物品上的时候,就失去了跳跃的功能。所以,我们也要monkey在物品上也有跳跃的功能,就像在floor上。将下段代码添加到Monkey.mm的最后,然后用相同的方式来计算numFloorContacts:
-(void) beginContactWithObject:(GB2Contact*)contact
{
// count object contacts as floor contacts
numFloorContacts++;
}
-(void) endContactWithObject:(GB2Contact*)contact
{
// count object contacts as floor contacts
numFloorContacts--;
}
运行一下,现在是不是更好点了?我们的游戏已经可以玩了~
现在我们来让monkey可以推动掉落的物品来回走动。现在在Monkey.h里添加下面变量:
int numPushLeftContacts;
int numPushRightContacts;
int numHeadContacts;
还记得我们在PhysicsEditor里面设置的Id值吗,现在有用了,将下面代码替换掉以前的:
-(void) beginContactWithObject:(GB2Contact*)contact
{
NSString *fixtureId= (NSString*)contact.ownFixture->GetUserData();
if([fixtureId isEqualToString:@"push_left"])
{
numPushLeftContacts++;
}
else if([fixtureId isEqualToString:@"push_right"])
{
numPushRightContacts++;
}
else if([fixtureId isEqualToString:@"head"])
{
numHeadContacts++;
}
else
{
// count others as floor contacts
numFloorContacts++;
}
}
-(void) endContactWithObject:(GB2Contact*)contact
{
NSString *fixtureId= (NSString*)contact.ownFixture->GetUserData();
if([fixtureId isEqualToString:@"push_left"])
{
numPushLeftContacts--;
}
else if([fixtureId isEqualToString:@"push_right"])
{
numPushRightContacts--;
}
else if([fixtureId isEqualToString:@"head"])
{
numHeadContacts--;
}
else
{
// count others as floor contacts
numFloorContacts--;
}
}
就像你看到的,你通过接触的物品的fixture然后access fixture的GetUserData方法。
现在我们可以跟踪这个contacts,我们来更新monkey的动作框架来解决额外的事件。下面是一些动作的设定:
Monkey |
Objects |
Animation |
Standing |
No object above head |
Idle |
Standing |
Object above head |
Arms up |
Moving |
No push sensor active in the direction |
Walk |
Moving |
Push sensor active in the direction |
Push |
通过上面的表格,我们来设定Monkey.mm的updateCCFromPhysics的#6:
// 6 - Update animation phase
const float standingLimit= 0.1;
NSString *frameName= nil;
if((vX >-standingLimit)&& (vX < standingLimit))
{
if(numHeadContacts >0)
{
// Standing, object above head
frameName = [NSString stringWithFormat:@"monkey/arms_up.png"];
}
else
{
// Just standing
frameName = [NSString stringWithFormat:@"monkey/idle/2.png"];
}
}
else
{
if(numFloorContacts== 0)
{
// Jumping, in air
frameName = [NSString stringWithFormat:@"monkey/jump/%@.png", dir];
}
else
{
// Determine if monkey is pushing an item
bool isPushing = (isLeft && (numPushLeftContacts > 0))
|| (!isLeft && (numPushRightContacts > 0));
// On the floor
NSString *action= isPushing ? @"push": @"walk";
frameName = [NSString stringWithFormat:@"monkey/%@/%@_%d.png", action, dir, animPhase];
}
}
现在再运行,perfect!monkey的动作就是我们想要的。
玩了一会,我想monkey应该可以在一些特定的状态下更强壮,现在,当有物品在他上方的时候,他太木有力气来跳开,我们让他在被困住的时候可以有更大的力气来跳出来。
下面这段代码是Monkey.mm里面的jump方法中来控制monkey jump的:
[self applyLinearImpulse:b2Vec2(0,[self mass]*JUMP_IMPULSE)
point:[self worldCenter]];
将它替换成如下的代码:
float impulseFactor = 1.0;
// if there is something above monkey's head make the push stronger
if(numHeadContacts >0)
{
impulseFactor = 2.5;
}
[self applyLinearImpulse:b2Vec2(0,[self mass]*JUMP_IMPULSE*impulseFactor)
point:[self worldCenter]];
现在我们的monkey吃了激素了,当有物品在它上方的时候,它就拥有了2.5倍原来的力气,这应该可以让它脱困了。
但是现在又有新的问题了,如果有个物品轻轻的挂在monkey的head上,它获取到的新的力量可以让它穿过屋顶!我们需要在updateCCFromPhysics方法中限定monkey的最大速度,将下面的代码添加到updateCCFromPhysics方法#3的最后:
const float maxVelocity= 5.0; float v = velocity.Length(); if(v > maxVelocity) { [self setLinearVelocity:maxVelocity/v*velocity]; } |
注意在上面的代码中,我们直接改变了Box2d引擎控制的值,以此来影响物理引擎的所有动作。你应该尝试避免做此类动作。现在运行,monkey反应快速,并且足够强壮来推开任何物品,并且木有失控。
第三章
在第一章中,我们介绍了monkeyjump的游戏设计,生成sprite sheets和shapes,并且开了一个coding的头。
在第二章中,我们在游戏中添加了我们的hero,来让它移动,跳跃,并且做一些游戏设置。
在这章,也是最后一章里,我们添加一些性能的改进,添加了一层HUD Layer,并且可以杀死monkey!:]
我们将会接着上一章继续。
玩我们的游戏一会,你就会发现游戏变得越来越慢,直到它不能玩为止。
这里有一个原因,物品一个接一个的从天上往下掉,他们会碰撞到地上已经存在的物品,他们所有的碰撞都会由Box2d来控制,如果有n个物品,那里将有n*(n-1)中碰撞的可能来处理。虽然Box2d使用hashes来使物品变快,但是你能想象随着物品的增加,会有多少动态的碰撞可能来被处理。
如果你看见一个物品掉落,你会发现这个个动作和反射都会通过叠加的推动力传递下来,通过叠加从一个对象到另一个对象。
为了改进这种状况,我们要将那些已经离monkey比较远的下面的物品设置为static的,这些static的物品依然允许其他的对象叠加在它的上面,但是它们不再对这种碰撞影响做出反应。那么落下来的物品将会只对这个堆的顶层的一部分有影响。
Box2d允许物品一些时候在不被触碰到的情况下可以go to sleep,我们将会使用这一特性来改进我们的游戏。
GB2Engine有一个迭代的方法来通过一块来迭代到所有的objects,我们会通过它来生成一个方法,用y坐标作为一个参数,来控制距离monkey以下一定范围外的objects go to sleep。
将下面的代码添加到GameLayer.mm的update方法的最后:
// 10 - Iterate over objects and turn objects into static objects
// if they are some distance below the monkey
float pruneDistance = 240/PTM_RATIO;
float prune =[monkey physicsPosition].y- pruneDistance;
[[GB2Engine sharedInstance] iterateObjectsWithBlock:^(GB2Node* n)
{
if([n isKindOfClass:[Object class]])
{
Object *o = (Object*)n;
float y = [o physicsPosition].y;
if(y < prune)
{
// set object to static
// if it is below the monkey
[o setBodyType:b2_staticBody];
}
}
}
];
这个方法还有一些情况下产生的效果不是我们需要的。例如当物品成一个tower的时候,monkey爬到了上面,一个正在下降的物品虽然也在monkey下面,并且一定距离,但是在空中就成了static object。
解决办法是:当他们的速度is low的时候,才会将物体转变成static。将第二个if结构改成如下格式:
if((y < prune)&&
([o linearVelocity].LengthSquared() <0.1))
{...}
这里还是有一些问题,monkey依然有可能被卡在一堆objects里面,而他又不足够强壮到将上面很多的objects举开,问题就来了。
这里有很多方法可以解决这种情况。一种是很简单,直接判定monkey die。另一种情况是让monkey瞬移到所有objects的上面,然后游戏继续。这个主意听起来不错。
为了完成这项工作,我们必须确保monkey瞬移到了所有的objects上面。否则它的position会将它直接嵌入到其它的物品里面,并且它永远不会break free!
转换到GameLayer.h,并且增加一个变量:
float highestObjectY; // y position of the highest object |
并且声明它:
@property (nonatomic, readonly)float highestObjectY; |
现在转换到GameLayer.mm:
@synthesize highestObjectY; |
现在将下面的代码替换掉update的#10:
// 10 - Iterate over objects and turn objects into static objects
// if they are some distance below the monkey
float pruneDistance = 240/PTM_RATIO;
float prune =[monkey physicsPosition].y- pruneDistance;
highestObjectY = 0.0f;
[[GB2Engine sharedInstance] iterateObjectsWithBlock:^(GB2Node* n)
{
if([n isKindOfClass:[Object class]])
{
Object *o = (Object*)n;
float y = [o physicsPosition].y;
// record the highest object
if((y > highestObjectY)&& ([o active]))
{
highestObjectY = y;
}
if((y < prune)&&
([o linearVelocity].LengthSquared() <0.1))
{
// set object to static
// if it is below the monkey
[o setBodyType:b2_staticBody];
}
}
}
];
这段新的代码通过每次check,设置最高的物品的y值,来决定最高的物品,它只针对active的物品,否则,最高的物品会始终是那个等待掉掉落的物品。
转到Monkey.h,添加下面一个新的变量:
int stuckWatchDogFrames;// counter to detect if monkey is stuck |
现在来让我们把被困住的monkey瞬移到最高的物品位置吧,将下面的代码添加到updateCCFromPhysics方法的最后:
// 8 - Check if monkey is stuck if(numHeadContacts >0) { stuckWatchDogFrames--; if(stuckWatchDogFrames== 0) { // teleport the monkey above the highest object [self setPhysicsPosition:b2Vec2([self physicsPosition].x, gameLayer.highestObjectY+2.0)]; } } else { // restart watchdog stuckWatchDogFrames = 120; // 2 seconds at 60fps } |
编译运行,现在好多了,如果monkey被困住了,并且它不能举开上面压住它的objects,那么它将会神奇般的自由。还有好多方法来检测monkey是否被困住,例如,我们可以来检测objects被堆起来的高度,或者猴子的速度保持很低的速度一定时间。自己可以试一下其他几种方法,来看看哪种最适合自己。
给我们的英雄造成点伤害吧,打开Monkey.h,并且添加一个新的变量:
float health; |
当然我们不能忘记下面两句声明:
@property (readonly)float health; @property (readonly) bool isDead; |
最后,我们define the maximum health,在文件的最上面,在包含命令的下面:
#define MONKEY_MAX_HEALTH 100.0f |
现在我们转换到Monkey.mm,然后添加下面一句话:
@synthesize health; |
实现isDead的属性,可以添加下面的代码,在walk方法的上面:
-(bool) isDead { return health <= 0.0f; } |
正如你注意到的,我们决定monkey是否死是基于它的health是否小于等于0.在init方法,初始化health:
// set health health = MONKEY_MAX_HEALTH; |
现在让我们来设定通过head的collision判断伤害到monkey,在beginContactWithObject:
else if([fixtureId isEqualToString:@"head"]) { numHeadContacts++; float vY =[contact.otherObject linearVelocity].y;
if(vY <0) { const float hurtFactor = 1.0; // reduce health health += vY*[contact.otherObject mass]* hurtFactor; if(self.isDead) { // set monkey to collide with floor only [self setCollisionMaskBits:0x0001]; // release rotation lock [self setFixedRotation:NO]; // change animation phase to dead [self setDisplayFrameNamed:@"monkey/dead.png"]; } } } |
基本上,monkey在有物体落到它头上的时候会受伤。使用这个物体的速度和质量来计算伤害,并且减去相应的血条。快速降落的物体可以伤害到它,但是放置在它头上的物体则不会。注意,我还加了一个hurtFactor来判断monkey会受伤多少。
如果monkey死亡,它会从scene上降落。在这种情况下,我们删除monkey的除了floor以外所有的碰撞对象。这将使monkey降落在floor上。我们将会释放不倾斜的锁,来让monkey躺在floor上,并且将它的图片变成dead.png。
死亡的monkey不能跳跃──因此当monkey死掉的时候我们要忽视对屏幕的点击事件,因此改变monkey的jump事件:
-(void) jump { if((numFloorContacts >0) && (!self.isDead)) { ... |
并且改变updateCCFromPhysics的#1内容:
-(void) updateCCFromPhysics { // 1 - Call the super class [super updateCCFromPhysics]; // he's dead – so just let him be! if(self.isDead) { return; }
... |
编译运行,现在你可以展现你邪恶的一面了──杀死monkey!:]
重新开始游戏,现在monkey死了,但是objects依然保持着下降,并且木有方法来重新开始游戏。
我建议在monkey死后2s重新开始游戏。一般,我们会在游戏结束后去分数面板,但是这样会有很多东西,一个简单的重新启动就能够满足了。
在GameLayer.h里建立一个启动时间:
ccTime gameOverTimer; // timer for restart of the level |
将下面的代码添加到GameLayer.mm的update的开始部分:
if(monkey.isDead) { gameOverTimer += dt; if(gameOverTimer >2.0) { // delete the physics objects [[GB2Engine sharedInstance] deleteAllObjects];
// restart the level [[CCDirector sharedDirector] replaceScene:[GameLayer scene]]; return; } } |
为了重新启动,我们简单的从GB2Engine删除所有的objects,并且用一个新的GameLayer来替代当前的scene。
运行一下。
The HUD Layer-Health
是的,monkey死了,但是木有人知道这是怎么发生的!这对我和绝大多数玩家来说都是很现实的。让我们增加一个血条的展示吧,这样我们就能一直记录monkey的health了。
我们准备用10个香蕉来表示monkey的health,每个香蕉代表10点血。
新建一个Hud类,让它成为CCSpriteBatchNode的子类。不要忘了将.m换成.mm。将Hud.h的内容替换成下面的代码:
#pragma once
#import "Cocos2d.h"
#define MAX_HEALTH_TOKENS 10
@interface Hud : CCSpriteBatchNode { CCSprite *healthTokens[MAX_HEALTH_TOKENS];// weak references float currentHealth; }
-(id) init; -(void) setHealth:(float) health;
@end |
HUD的display使用的是来自jungle的表,所以我们必须使HUD继承CCSpriteBatchNode为了可以使用jungle表格中的精灵。除此之外,HUD需要记录monkey的当前health,并且这个精灵代表monkey的每一个血条。我们也需要一个方法来改变当前的血条。
转换到Hud.mm,并且将它的内容替换为如下代码:
#import "Hud.h"
#import "Monkey.h"
#import "GMath.h"
@implementation Hud
-(id) init
{
self = [super initWithFile:@"jungle.pvr.ccz" capacity:20];
if(self)
{
// 1 - Create health tokens
for(int i=0; i<MAX_HEALTH_TOKENS; i++)
{
const float ypos = 290.0f;
const float margin = 40.0f;
const float spacing = 20.0f;
healthTokens[i]= [CCSprite spriteWithSpriteFrameName:@"hud/banana.png"];
healthTokens[i].position= ccp(margin+i*spacing, ypos);
healthTokens[i].visible= NO;
[self addChild:healthTokens[i]];
}
}
return self;
}
@end
这里我们用sprite sheet初始化HUD的CCSpriteBatchNode父类。
我们循环访问里面的香蕉精灵,并且每次循环设置一下当前单个香蕉精灵的位置。最后的效果是一横排香蕉,作为monkey的health。
最后我们添加如下代码到Hud.mm的@end之前:
-(void) setHealth:(float) health
{
// 1 - Change current health
currentHealth = health;
// 2 - Get number of bananas to display
int numBananas= round(MAX_HEALTH_TOKENS* currentHealth / MONKEY_MAX_HEALTH);
// 3 - Set visible health tokens
int i=0;
for(; i<numBananas; i++)
{
healthTokens[i].visible= YES;
}
// 4 - Set invisible health tokens
for(; i<MAX_HEALTH_TOKENS; i++)
{
healthTokens[i].visible= NO;
}
}
在这个方法中,我们需要决定香蕉的展示数量,使一些可以显示,另一些不能展示的被删掉。#3和#4很重要。
接下来我们要在GameLayer上添加一个新的HUD。转换到GameLayer.h并且添加下面一句代码:
@class Hud; |
添加一个HUD的变量声明:
Hud *hud; |
转换到GameLayer.mm并且包含Hud.h:
#import "Hud.h" |
在init方法中初始化HUD:
// add hud hud = [[[Hud alloc] init] autorelease]; [self addChild:hud z:10000]; |
最后将下面代码放在update方法的最后:
// 11 - Show monkey's health in bananas [hud setHealth:monkey.health]; |
运行,功能顺利,但是我不喜欢界面,我不认为香蕉应该突然出现或者消失,我想让他们淡入淡出。
将setHealth的#3和#4替换成以下代码:
// 3 - Set visible health tokens
int i=0;
for(; i<numBananas; i++)
{
if(!healthTokens[i].visible)
{
healthTokens[i].visible= YES;
healthTokens[i].scale= 0.6f;
healthTokens[i].opacity= 0.0f;
// fade in and scale
[healthTokens[i] runAction:
[CCSpawn actions:
[CCFadeIn actionWithDuration:0.3f],
[CCScaleTo actionWithDuration:0.3f scale:1.0f],
nil]];
}
}
// 4 - Set invisible health tokens
for(; i<MAX_HEALTH_TOKENS; i++)
{
if(healthTokens[i].visible&& (healthTokens[i].numberOfRunningActions== 0))
{
// fade out, scale to 0, hide when done
[healthTokens[i] runAction:
[CCSequence actions:
[CCSpawn actions:
[CCFadeOut actionWithDuration:0.3f],
[CCScaleTo actionWithDuration:0.3f scale:0.0f],
nil],
[CCHide action]
, nil]
];
}
}
现在就可以运行一下。
The Score:转换到Monkey.h,添加一个变量和属性:
float score; |
@property (nonatomic, readonly)float score; |
转换到Monkey.mm,在开始的地方添加下面一段代码:
@synthesize score; |
码将下面的代码添加到updateCCFromPhysics最后:
// 9 - update score
if(numFloorContacts >0)
{
float s =[self physicsPosition].y* 10;
if(s> score)
{
score = s;
}
}
hysics最后:注意我们只有在新分数比当前分数高的时候才更新score,因为有可能monkey还会drop down。
转换到Hud.h,加入下面一句代码:
#define MAX_DIGITS 5 |
添加下列变量来保存数字精灵并且缓存CCSpriteFrame指针:
CCSprite *digits[MAX_DIGITS]; // weak references CCSpriteFrame *digitFrame[10];// weak references |
添加下列方法来设置分数:
-(void) setScore:(float) score; 置现在转换到Hud.mm。第一件事是cache数字精灵。将下面的几行添加到init方法的最后。 // 2 - Cache sprite frames CCSpriteFrameCache *sfc = [CCSpriteFrameCache sharedSpriteFrameCache]; for(int i=0; i<10; i++) { digitFrame[i]= [sfc spriteFrameByName: [NSString stringWithFormat:@"numbers/%d.png", i]]; }
// 3 - Init digit sprites for(int i=0; i<MAX_DIGITS; i++) { digits[i]= [CCSprite spriteWithSpriteFrame:digitFrame[0]]; digits[i].position= ccp(345+i*25,290); [self addChild:digits[i]]; } |
的最将下面的方法添加到该文件的最后-它以单个单个的字符打印出当前的分数:
-(void) setScore:(float) score { char strbuf[MAX_DIGITS+1]; memset(strbuf,0, MAX_DIGITS+1);
snprintf(strbuf, MAX_DIGITS+1,"%*d", MAX_DIGITS, (int)roundf(score)); int i=0; for(; i<MAX_DIGITS; i++) { if(strbuf[i]!= ' ') { [digits[i] setDisplayFrame:digitFrame[strbuf[i]-'0']]; [digits[i] setVisible:YES]; } else { [digits[i] setVisible:NO]; } } } |
个最后转换到GameLayer.mm,在update方法最后添加下面一句代码:
// 12 - Show the score [hud setScore:monkey.score]; |
编译运行。
当前monkey被掉下来的香蕉砸伤了,但是我希望他们能被monkey消费。
我们将创建一个object的子类ConsumableObject。这个类有个bool变量,来确认该object是否被消费。
我喜欢一个类一个文件,但是这些类太小,所以我把他们都添加到object.h的@end之后。
@interface ConsumableObject : Object
{
@protected
bool consumed;
}
-(void)consume;
@end
类一个文件同样的,创建一个Banana和一个BananaBunch的类在ConsumableObject之后:
@interface Banana : ConsumableObject
{
}
@end
@interface BananaBunch : ConsumableObject
{
}
@end
现在实现ConsumableObject在Object.mm里。
@implementation ConsumableObject
-(void) consume
{
if(!consumed)
{
// set consumed
consumed = YES;
// fade & shrink object
// and delete after animation
[self runAction:
[CCSequence actions:
[CCSpawn actions:
[CCFadeOut actionWithDuration:0.1],
[CCScaleTo actionWithDuration:0.2 scale:0.0],
nil],
[CCCallFunc actionWithTarget:self selector:@selector(deleteNow)],
nil]
];
// play the item comsumed sound
// pan it depending on the position of the monkey
// add some randomness to the pitch
[[SimpleAudioEngine sharedEngine] playEffect:@"gulp.caf"
pitch:gFloatRand(0.8,1.2)
pan:(self.ccNode.position.x-240.0f)/ 240.0f
gain:1.0];
}
}
@end
.mm现在转换到Monkey.h,并且添加一个restoreHealth方法:
-(void)restoreHealth:(float)amount; |
在Monkey.mm里实现上述方法:
-(void) restoreHealth:(float)amount
{
health = MAX(health+ amount, MONKEY_MAX_HEALTH);
}
在Object.mm里包含Monkey.h:
#import "Monkey.h" |
然后在Banana和BananaBunch实现beginContactWithMonkey方法:
@implementation Banana
-(void) beginContactWithMonkey:(GB2Contact*)contact
{
if(!consumed)
{
Monkey *monkey = (Monkey *)contact.otherObject;
[monkey restoreHealth:20];
[self consume];
}
}
@end
@implementation BananaBunch
-(void) beginContactWithMonkey:(GB2Contact*)contact
{
if(!consumed)
{
Monkey *monkey = (Monkey *)contact.otherObject;
[monkey restoreHealth:60];
[self consume];
}
}
@end
运行,木有效果!返回Object.mm并且修改randomObject方法来产生Banana和BananaBunch对象:
+(Object*) randomObject { NSString *objName; switch(rand()% 18) { case 0: // create own object for bananas - for separate collision detection return [[[Banana alloc] initWithObject:@"banana"] autorelease];
case 1: // create own object for banana packs - for separate collision detection return [[[BananaBunch alloc] initWithObject:@"bananabunch"] autorelease];
case 2:case 3:case 5: ... |
现在就ok啦,但是还有一件事情困扰我,当monkey碰到香蕉的时候,monkey就会stop,Box2D有两个phases在停止期间:一个preserve,一个collision。在preserve中,是可以使两个objects的碰撞失效的。这个碰撞会被call back,但是两个物体不会弹开。将下列的代码添加到Object.mm中的ConsumableObject里,在@end之前:
-(void) presolveContactWithMonkey:(GB2Contact*)contact { [contact setEnabled:NO]; } 最后的改进: 这是一个靠机会多过靠技术来胜利的游戏。为了让这个游戏有可玩性,我们在顶部加一个红色的指示器。转到GameLayer.h,添加下面的代码:
的将下面的代码添加到GameLayer.mm的init方法的最后: |
// object Hint objectHint = [CCLayerColor layerWithColor:ccc4(255,0,0,128) width:10.0f height:10.0f]; [self addChild:objectHint z:15000]; objectHint.visible=NO; |
在update方法的#8的上方添加下面的代码:
if(nextDrop < dropDelay*0.5) { // update object hint [objectHint setVisible:YES];
// get object's width float w = nextObject.ccNode.contentSize.width;
// and adjust the objectHint according to this [objectHint changeWidth:w]; objectHint.position = ccp([nextObject physicsPosition].x* PTM_RATIO-w/2,310); } else { [objectHint setVisible:NO]; } |
的当下一个掉落的时间少于dropdelay的一半的时候,我们将他设置在屏幕的上方,而且中心点在物体的x轴上。最后一件事情-音乐!将下一段代码添加到GameLayer.mm中:
#import "SimpleAudioEngine.h" |
将下一段代码添加到init方法的最后:
// music
[[SimpleAudioEngine sharedEngine] playBackgroundMusic:@"tafi-maradi-loop.caf"];