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

论述如何基于3种开发工具制作《Monkey Jump》(二)

发布时间:2012-02-14 17:11:09 Tags:,,,

作者:Andreas Loew

在本系列教程中,我们使用Cocos2D、TexturePacker和PhysicsEditor来制作一款有趣的横向平台游戏。

在系列教程的第1部分中,我们介绍了这款游戏的设计,制作了精灵层次和我们需要的形状,并开始编写游戏代码。(请点击阅读第3部分

在上个教程结束前,我们设立了所有的游戏层次,刚刚完成了让随机物体从天空上掉落的制作,还配上音效。

在本教程的第2部分中,我们将把主角添加到游戏中,让它能够移动和跳跃,并开始添加部分游戏玩法。

我们将以上次制作完成的项目为基础开始本教程。如果你还未获得所需内容,可以下载本系列教程的源代码并打开3-DraggingObjects。

开始

在本教程的第1部分中,我们用PhysicsEditor制作了猴子的形状,还是还未将猴子添加到游戏中。现在,让我们来实现这个目标!

以iOS\Cocoa Touch\Objective-C类模板创建新文件,由此来添加Monkey类。将类命名为“Monkey”,将其作为GB2Sprite的次类。记住,同样将Monkey.m文件的扩展名改为.mm。

这个猴子会根据游戏世界中的不同事件做出反应,比如有东西从上方落下时它会举起双手、推动道具和跳跃等。这便是你为何无法使用Cocos2d标准动画例行程序的原因,你需要自行编写执行程序。

为实现上述目标,你需要些许成员变量来存储额外的数据。将以下代码粘贴至Monkey.h中,替换原有代码:

#pragma once

#import “Cocos2d.h”
#import “GB2Sprite.h”

@class GameLayer;

@interface Monkey : GB2Sprite
{
float direction;      // keeps monkey’s direction (from accelerometer)
int animPhase;        // the current animation phase
ccTime animDelay;     // delay until the next animation phase is stated
GameLayer *gameLayer; // weak reference
}

-(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 – Initialize the monkey
self = [super initWithDynamicBody:@"monkey"
spriteFrameName:@"monkey/idle/1.png"];

if(self)
{
// 2 – Do not let the monkey rotate
[self setFixedRotation:true];

// 3 – The monkey uses continuous collision detection
// to avoid getting stuck inside fast-falling objects
[self setBullet:YES];

// 4 – Store the game layer
gameLayer = gl;
}

return self;
}

@end

我们来逐步解释initWithGameLayer方法:

1、首先,我们将猴子初始化。猴子的移动会受到物理引擎的影响,所以要将其制作成动态物体。我们使用空闲帧来充当猴子动画和猴子物理形状的首帧。

2、猴子应当保持竖直站立状态,所以我们设定其不可旋转。这意味着猴子会通过Box2d移动,但不会旋转或倾斜。

3、将猴子设置为子弹模式。子弹模式支持对象的持续性碰撞检测。如果不这样设置的话,Box2d会先移动对象然后再进行碰撞检测。对于快速移动的对象来说,可能出现对象因毫无碰撞检测直接穿过另一个对象或卡在另一个对象中。持续性碰撞检测会在对象从当前位置移动到新位置的整个过程中计算碰撞,而不只是在终点。

4、最后,你需要存储游戏层次,分配数值即可。

MonkeyBullet(from raywenderlich)

MonkeyBullet(from raywenderlich)

对于第3个步骤中的子弹模式,如果你正在编写代码的项目中对象数量较少,你可以将Box2d引擎设置为对所有游戏对象运行持续性碰撞检测。但是,当游戏拥有大量对象时,这会给CPU增加负担。因此,在我们这款游戏中,我们只将猴子和快速移动(游戏邦注:从屏幕上方掉落)的对象设置为持续性模式。

要使猴子可用,必须将它添加至GameLayer。打开GameLayer.h,将下列代码添加在顶端的输入声明下:

@class Monkey;

现在,向GameLayer类添加下列成员变量:

Monkey *monkey;

然后,转换至GameLayer.mm,在文件顶端输入Monkey.h:

#import “Monkey.h”

在GameLayer.mm中初始化选择器末端,将猴子初始化,添加到游戏层次中并为猴子设置起始点:

monkey = [[[Monkey alloc] initWithGameLayer:self] autorelease];
[objectLayer addChild:[monkey ccNode] z:10000];
[monkey setPhysicsPosition:b2Vec2FromCC(240,150)];

编译运行,你会看到以下画面:

monkey jump (from raywenderlich)

monkey jump (from raywenderlich)

猴子出现在屏幕上!物体掉落到他头上,它会将其推开。这就是我们想要的结果。

猴子的行走

我们的下个目标是让猴子开始行走,使用加速计作为输入方式。

回到GameLayer.mm中,将下列代码添加到初始化方法末端:

self.isAccelerometerEnabled = YES;

这可以确保,对于内置加速计值的每次改变,GameLayer类都能够自动获得通知。接下来,我们要在GameLayer.mm文件末端添加通知处理器,位置在@end标记之前:

- (void)accelerometer:(UIAccelerometer*)accelerometer
didAccelerate:(UIAcceleration*)acceleration
{
// forward accelerometer value to monkey
[monkey walk:acceleration.y];
}

加速计处理器会调用带有加速计Y轴数值的猴子对象行走方法。这种方法会根据加速计输入来处理和实现猴子的前后移动。

接下来,转换到Monkey.mm,在文件末端添加行走方法(游戏邦注:在@end标记之前)。这个方法会将猴子的新移动方向存储在成员变量中。

-(void) walk:(float)newDirection
{
direction = newDirection;
}

现在尝试编译运行代码,结果什么也没发生。原因在于,尽管方向值已被存储,但是它还未被运用到物理模拟中。要以新移动方向为基础来更新物理模拟,我们需要覆写updateCCFromPhysics选择器,GB2Engine每帧都会调用来更新物理情况。

更新猴子物理效果

将下列代码添加到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]];
}

在上述代码中,首先你调用超类选择器,根据物理模拟更新猴子的精灵。

随后,你根据存储的方向值将猴子推向正确的方向。重点在于,不要对猴子进行完全的控制。否则,其响应事件(游戏邦注:比如道具掉落或碰撞检测)的“自然”行为无法合理地运行。

你真正做的事情就是,将猴子轻推向正确的方向。因为物理引擎的更新频率为每秒60次,所以保持推力较小很重要。这只是个猴子,不是子弹,尽管我们采用的是子弹模式!

你可以通过对物体施加推力来移动box2D物体。你使用GB2Sprite的applyLinearImpulse方法来实现这个目标,涉及以下两个参数:施加的推力和施力点。

对于施力点,我们将使用物体的重心。将力施加在物体的重心上,这样就不会产生导致物体选择的扭转力。但是,这种情况在我们的游戏中肯定不会出现,因为我们已经将猴子设置为不可旋转。

ApplyLinearImpulse(from raywenderlich)

ApplyLinearImpulse(from raywenderlich)

对于施加的推力,我推荐使用物体的质量来计算。这是因为推力就是质量和速度的产物。

将质量值与存储的方向值相乘。这样当设备稍微倾斜时,产生的推力就比较小,当设备大幅倾斜时,就会产生更大的推力。

以物体质量为基础来缩放推力,使我们无需担心在PhysicsEditor中改变物体形状时会影响物体的移动。如果我们不采取这种做法,假设随后我们将猴子做得更小些,相同的推力施加在质量较小的猴子上,会导致其移动过快。

我们还要设定最大值,避免施力过大。最大施力由MAX_WALK_IMPULSE变量确定。

编译运行,还是毫无变化。原因在于,iPhone模拟器无法模拟加速计。所以,从现在开始,你需要在设备上进行测试!换成设备,再次测试游戏。

猴子现在可以左右滑动,但是移动看起来很不自然。

猴子的移动

接下来,我们要向Monkey.mm添加些许代码,让猴子的动画开始运转。在updateCCFromPhysics方法后添加下列代码:

animDelay -= 1.0f/60.0f;
if(animDelay <= 0)
{
animDelay = ANIM_SPEED;
animPhase++;
if(animPhase > 2)
{
animPhase = 1;
}
}

首行代码通过将时延削减到下个动画阶段来更新下个动画的时间。我使用1.0f/60.0f这个值,是因为我猜想应用以60 fps的速度运行,而且updateCCFromPhysics方法不含有能够精确提供每次更新时间间隔的delta时间参数。

如果动画时延下降到0以下,那么动画延迟值便会重置到动画速度,并将当前阶段加1。如果已经达到最高阶段,将循环回最低阶段,这样动画才能不断循环播放。

接下来,我们需要确定猴子面部的朝向。这可以通过以下两种方法实现:

1、使用来自加速计的方向

2、使用猴子的速度矢量

我偏向于使用加速计,因为它会在玩家尝试通过倾斜设备改变方向时立即提供反馈。接下来,我们将同通过加速计对速度改变做出响应。

在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];

从根本上说,以上代码实现的是:如果猴子的速度低于standingLimit,它会在空闲动画帧时直视玩家。否则,它会使用与当前方向和动画帧数相符的行走呈现帧。

编译运行。现在,猴子可以自然地奔跑!

放慢移动速度

对于这个结果我仍有不满意的地方,我觉得猴子移动得太快了。我们可以减小施加到他身上的推力,但是这也会让它的移动显得缓慢而笨拙。

我们需要足够大的推力使他快速反应,但是不要过快地移动。

将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变量的代码,将推力代码移动到第4部分囊括在if条件中,如果当前方向的速度低于最大值时就算条件满足。这会防止猴子本身的加速过快。

编译运行。我想现在看起来要好得多。

monkey jump (from raywenderlich)

monkey jump (from raywenderlich)

项目当前状态的源代码在4-WalkingMonkey文件夹中。

跳跃

现在,我们要让猴子能够跳跃。对于这个目标,我们需要让游戏检测到GameLayer上任何地方的接触事件,因为我们想要让猴子在每次接触时跳跃。

打开GameLayer.mm,将下列代码添加到初始化选择器中以激活接触检测:

// enable touches
self.isTouchEnabled = YES;

将下列选择器添加到文件末尾(游戏邦注:在@end标记前)。它通过跳跃方法将接触推向猴子物体。

-(void)ccTouchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
[monkey jump];
}

此刻,你或许会注意到我们的Monkey类中并没有跳跃方法。我们接下来变要添加这个方法。转换到Monkey.h,将下列方法定义添加到@end前:

-(void) jump;

现在,打开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 ];
}

上述代码实现的是施加推力让猴子跳跃,同时播放跳跃的音效。我们在本教程的第1部分中已经制作了该音效。

编译运行。触动屏幕让猴子跳跃。

monkey jump(from raywenderlich)

monkey jump(from raywenderlich)

修正跳跃问题

但是,这样的结果并不完美:如果你多次触动屏幕,猴子会跳得过高。更为糟糕的是,镜头不会跟随猴子移动。

让我们先来固定镜头。首先,将下列代码添加到GameLayer.mm中的更新选择器顶端,位于现有代码之上。

// 0 – monkey’s position
float mY = [monkey physicsPosition].y * PTM_RATIO;

上述代码将猴子的Y轴位置值复制到新变量mY中。当然,我们也可以访问猴子的ccNode,从那里获取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;
}

这样,我们就可以实现镜头Y轴坐标的计算,保持猴子位于屏幕的中心。将该数值设置为正数,这样镜头就不会移动到地面以下。

接下来,让我们来执行背景的视差滚动,这样猴子的移动会显得更加自然。这种效果很容易实现,只需要将背景层次同小于1.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

根据你的需要来调整数值。

编译运行。多次触动屏幕看看猴子能否自然跳跃。

道具的堆积

如果你花点时间来体验游戏,你会注意到道具不断从相同的高度处下落,有时甚至低于猴子的当前位置。事实上,经过一段时间后,道具甚至无法下落,因为它们都堆积在生成点上。

改变GameLayer.mm更新方法中的下列代码来解决这个问题:

float yPos = 400;

将上述代码换成以下代码:

float yPos = 400 + mY;

现在,无论猴子位于什么地方,道具都会在离猴子头顶400pt的位置生成。

回到猴子上。你已经注意到猴子在空中时依然可以保持不断跳跃。这是不合理的。我们需要进行修正,猴子只有在接触到地面的时候才能够跳跃。

让我们从计算猴子与地面的接触数量开始。

给Monkey.h添加新变量:

int numFloorContacts;     // number of floor contacts

转换到Monkey.mm,将下列两个新碰撞检测处理器添加到文件末端(游戏邦注:@end标记前):

-(void) beginContactWithFloor:(GB2Contact*)contact
{
numFloorContacts++;
}

-(void) endContactWithFloor:(GB2Contact*)contact
{
numFloorContacts–;
}

正如其名称所表示的那样,第1个检测猴子与地面接触/碰撞的起点,第2个检测碰撞重点。在beginContact选择器中,我们增加了地面接触变量的数值,然后降低其在endContact选择器中的数值。

在猴子与地面每次接触的开始或结束,GBox2D会调用这些选择器。记住:我们之前为地面制作了独立的类,这样GBox2D现在就能够调用带有类名称的恰当选择器。

现在,如果猴子站在地面上,那么numFloorContacts值应当至少是1。将这个代码囊括到Monkey.mm的跳跃方法中,添加if条件,查看猴子在跳跃前是否真正站在地面上:

-(void) jump
{
if(numFloorContacts > 0)
{
[self applyLinearImpulse:b2Vec2(0,[self mass]*JUMP_IMPULSE)
point:[self worldCenter]];


}
}

编译运行,效果看起来很不错。只是,当猴子站在物体上时,它失去了跳跃的能力。要解决这个问题,我们需要将猴子与物体的接触视为同地面的接触。

这执行起来很容易。只需要在Monkey.mm末端添加更多的碰撞处理例行程序,将物体接触视为地面接触来计算即可:

-(void) beginContactWithObject:(GB2Contact*)contact
{
// count object contacts as floor contacts
numFloorContacts++;
}

-(void) endContactWithObject:(GB2Contact*)contact
{
// count object contacts as floor contacts
numFloorContacts–;
}

编译运行,现在游戏看起来很棒,我们已经得到了可以玩的游戏!

推动道具

接下来,让我们来改善游戏玩法,让猴子可以左右推动道具,让他可以将双手放在头顶,保护自己免被下落的道具砸中。

你还记得第1部分中你是如何在猴子的左右侧添加传感器的吗?现在他们派上用场了!传感器的关键在于,在PhysicsEditor中设置“Id”参数。现在,你就要检索这个数值!

但是,在我们这么做之前,我们需要添加些许实例变量来计算左右传感器和猴子头部的接触数量。在Monkey.h中添加这些变量:

int numPushLeftContacts;
int numPushRightContacts;
int numHeadContacts;

接下来,beginContactWith和endContactWith选择器拥有我们可以用来确定猴子哪个部分与物体接触的接触参数,我们添加到PhysicsEditor的“Id”值以用户数据的形式存储在每个固定装置中。所以,将Monkey.mm现有的物体接触处理器替换为下列代码:

-(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–;
}
}

从新代码中可以看到,你检索了Id,在这里我们将调用fixtureId,通过访问接触参数的固定装置随后通过GetUserData方法访问固定装置用户数据来实现。

现在,我们正在跟踪接触,我们可以更新猴子的动画帧来处理这些额外的事件。

以下是各种动画的对应表格:

objects(from raywenderlich)

objects(from raywenderlich)

使用上述表格,我们将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];
}
}

编译运行。简直堪称完美!现在猴子的表现正是我们想要的结果。

monkey jump(from raywenderlich)

monkey jump(from raywenderlich)

让猴子更为强壮

但是,在玩一段时间后,我觉得猴子在某些条件下应当更加强壮。现在,他还很弱小,当物体落在他头上时无法轻易脱困。让我们给这种条件下的猴子额外的力量。

以下是目前Monkey.mm中控制猴子跳跃的跳跃选择器代码:

[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]];

这就我们的猴子来说就像是兴奋剂!现在,当有物体落在他头上时他可以使用是原来2.5倍的推力,这应当能够帮助他应对多数物体。

我们也改变下猴子需要推动物体时的行走推力。转向updateCCFromPhysics剪切第6部分的下列代码:

// Determine if monkey is pushing an item
bool isPushing =  (isLeft && (numPushLeftContacts > 0))
|| (!isLeft && (numPushRightContacts > 0));

将该代码粘贴到第4部分,修改如下所示:

// 4 – Determine direction of the monkey
bool isLeft = (direction < 0);

// Determine if monkey is pushing an item
bool isPushing =  (isLeft && (numPushLeftContacts > 0))
|| (!isLeft && (numPushRightContacts > 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);
if(isPushing)
{
impulse *= 2.5;
}
[self applyLinearImpulse:-b2Vec2(impulse,0) point:[self worldCenter]];
}

编译运行,这样游戏看起来会好很多。但是仍然有个问题,当猴子的头部轻擦坠落的物体时,新的跳跃力量会让它跳出屏幕边界!

我们需要在updateCCFromPhysics方法中限制他的最大速度。将下列代码添加到updateCCFromPhysics方法第3部分的末端:

const float maxVelocity = 5.0;
float v = velocity.Length();
if(v > maxVelocity)
{
[self setLinearVelocity:maxVelocity/v*velocity];
}

应当注意的是,在上述代码中,我们直接修改了由Box2d引擎控制的数值,因而会影响到物理引擎的整体行为。你应当尽量避免进行此类改动。

编译运行。我比较喜欢猴子现在的行为。他反应快速,且足够强壮来推动物体,但是不会变得无法控制。

下一步计划

你可以下载本教程的所有源代码。

本部分教程已接近尾声!项目当前形势的源代码在名为5-MonkeyJumpAndRun的文件夹中。

在本系列教程的最后一部分中,我们将添加某些表现上的提升,为游戏添加HUD层次。(本文为游戏邦/gamerboom.com编译,拒绝任何不保留版权的转载,如需转载请联系:游戏邦

How To Build a Monkey Jump Game Using Cocos2D, PhysicsEditor & TexturePacker Part 2

Andreas Loew

Welcome back to the Monkey Jump tutorial! In this series, we are creating a fun vertical scrolling platformer with Cocos2D, TexturePacker and PhysicsEditor.

In Part One on the tutorial series, we introduced the MonkeyJump! game design, created the sprite sheets and shapes we needed, and began coding the game.

Before we stopped for a break, we had all of our game layers set up and had just finished making random objects drop from the sky, with sound effects.

In this second part of the tutorial, we will add our hero to the game, make him move and jump, and start adding some gameplay.

We’ll be starting with the project where we left off last time. If you don’t have it already, grab the source code for this tutorial series and open up 3-DraggingObjects.

Without further ado, let’s get back to (monkey) business! :]

Getting Started

We created the monkey’s shape in PhysicsEditor in Part One of this tutorial, but haven’t added the monkey to the game yet. Let’s do that now!

Add the Monkey class by creating a new file with the iOS\Cocoa Touch\Objective-C class template. Name the class Monkey, and make it a subclass of GB2Sprite. (Again, remember to change extension for the Monkey.m file to .mm)

The monkey will react to different events in the game world: he’ll put up his hands when something drops from above, push items, jump, etc. This is why you won’t be using Cocos2d’s standard animation routines, and instead implement some of your own.

For this, you need some member variables to store the additional data. Paste this code into Monkey.h replacing what’s there already:

Now switch to Monkey.mm and replace it with the following lines:

Let’s go through the initWithGameLayer method step-by-step:

First, we initialize the monkey. The monkey’s movement will be affected by the physics engine, so make him a dynamic object. We’ll use the idle frame as both the first frame of the monkey animation and the monkey’s physics shape.

The monkey should stand straight up all the time, so we set his rotation to fixed. This means the monkey is moved by Box2d, but does not rotate or tilt.

Set the monkey to bullet mode. Bullet mode enables continuous collision detection on an object. Without it, Box2d moves objects and then performs the collision checks. With fast-moving objects it’s possible that an object will pass through another without any collision detection at all or that an object will get stuck in another. Continuous collision detection calculates collisions all the way from an object’s current position to its new position – not just for the end point.

Finally, you need to store the game layer – keep it as a weak reference and just assign the value.

With regards to bullet mode for step #3, if you were coding a project with only a few objects, you could set the Box2d engine to run continuous collision detection for all game objects. However, when a game has a lot of objects that would add a lot of CPU overhead. So, for our game we’ll set the continuous mode on just the monkey and fast-moving (dropping) objects.

To enable the monkey, add him to the GameLayer. Open GameLayer.h and add the following lines at the top, just below the import statements:

Now, add the following member variable to the GameLayer class:

Then switch to GameLayer.mm and import Monkey.h at the top of the file:

At the end of the init selector in GameLayer.mm, initialize the monkey, add him to the game layer and set a starting position for the monkey:

Compile and run, and you’ll see the following:

The monkey is in the house! Objects drop onto him and he gets pushed away – perfect. That’s exactly what we want.

Do the Monkey Walk

Our next goal is to make the monkey walk, using the accelerometer as input.

Go back to GameLayer.mm and add the following code at the end of the init method:

This will ensure that for each change in the built-in accelerometer values, the GameLayer class gets an automatic notification. The notification handler has to be added to GameLayer.mm at the end of the file, before the @end marker:

The accelerometer handler calls the walk method of the monkey object with the y-axis value of the accelerometer. This method will handle actually moving the monkey back and forth based on the accelerometer input.

So, move to Monkey.mm and add the walk method to the end of the file (before the @end marker). This method simply stores the new movement direction for the monkey in a member variable.

Try compiling and running the code now … Surprise! Nothing new happens. This is due to the fact that while the direction value has been stored, it has not been applied to the physics simulation yet. In order to update the physics simulation based on the new movement direction, we need to override the updateCCFromPhysics selector, which is called by the GB2Engine on every frame for a GB2Node object to update the physics.

Update Monkey Physics

Add the following code to Monkey.mm:

In the above, you first call the selector of the super class. This will update the monkey’s sprite based on physics simulation.

Then, you push the monkey in the right direction based on the stored direction value. It is important not to take complete control of the monkey. Otherwise, his “natural” behavior in response to events such as items dropping or collision detection won’t work properly.

All you really do is to give the monkey a nudge in the right direction. Since the physics engine updates happen 60 times per second, it’s important to keep the push quite light. It’s a monkey – not a bullet, even if he is in bullet mode!

You can move a box2D object by applying an impulse to the object. And you do this using the GB2Sprite’s applyLinearImpulse method, which takes two parameters: the impulse to apply and the point of application.

For the point of application, we’ll use the world center of the object. Applied at the world center, the object will be pushed without any torque that would result into a rotation. (Which, by the way, would not happen to the monkey anyway, since we already set him not to rotate.)

When applying an impulse, I recommend using the mass of the object, which you can get with [self mass]. This is because the impulse is the product of mass and velocity.

Multiply the mass value by the stored direction value. This gives you small impulses when the device is tilted slightly, and bigger impulses when its tilted sharply.

Scaling the impulse with the object’s mass frees us from having to worry about the movement changing when we change the object’s shape in PhysicsEditor. If we didn’t scale the impulse and later we made the monkey a bit smaller (for example), the same impulse applied to a monkey of less mass will result in a faster-moving monkey.

We will also clamp the value to a maximum to avoid impulses that are too strong. The maximum impulse is defined with the MAX_WALK_IMPULSE variable.

Compile and run. Still nothing? Ah, I forgot to tell you one thing: the iPhone simulator does not simulate the accelerometer. So, from now on, we need to test on the device! Switch to the device and test.

The monkey now slides left and right – but the movement doesn’t look very natural.

Make the Monkey Move

We’re going to add some code to Monkey.mm to get the monkey animations working. Add the following to the end of the updateCCFromPhysics method:

The first line simply updates the time till the next animation by decreasing the time delay till the next animation phase. I use the value 1.0f/60.0f because I assume that the application runs at 60 fps and the updateCCFromPhysics method does not have a delta time parameter which would provide the timer interval between each update accurately.

If the animation time delay drops below zero, reset the animation delay value to the animation speed and increase the current phase by one. If the highest phase is reached, loop back to 1 so that the animation will continue to play in a loop.

Next, we need to determine the direction the monkey is facing. There are two ways to do this:

Use the direction from the accelerometer

Use the monkey’s velocity vector

I prefer to use the accelerometer, since it gives the player immediate feedback when he or she tries to change the direction by tilting the device. We’ll respond to velocity changes via accelerometer later.

Add this code to the end of updateCCFromPhysics:

Basically, all the code above does is, if the monkey’s speed is lower than standingLimit, it makes him look directly at the player with his idle animation frame. Otherwise, it uses a walk display frame matching the current direction and animation frame number.

Compile and run. The monkey now runs about – nice!

Slow Down Little Fella

I’m still not happy with one thing: I think the monkey moves too fast. We could reduce the impulse we’re applying to make him walk, but this will also make him slow and clumsy.

We need a strong enough impulse to make him react fast – but not too fast.

Replace the current code for updateCCFromPhysics in Monkey.mm with the following code:

As you’ll notice, we’ve moved a few code blocks around but the major change is the addtion of a new line to section #3 for the vX variable and moving the impulse code to section #4 to be wrapped within an if condition that checks if the velocity is below a maximum value for the current direction. This allows the monkey to steer against being pushed away by an object, but keeps him from accelerating too fast on his own.

Compile and run. I think this looks much better now.

The source code of the project in its current state is available in the folder 4-WalkingMonkey.

Jump, Jump!

Now, let’s make the monkey jump. For this, we need to make the game detect touch events anywhere on the GameLayer, because we want the monkey to jump at every touch.

Open GameLayer.mm and enable touch detection by adding the following line to the init selector:

Also add the following selector at the end of the file (but before the @end marker). It forwards the touches to the monkey object via the jump method.

You might notice at this point that we don’t have a jump method in the Monkey class. That’s what we’ll add next. Switch to Monkey.h and add the following method definition just before @end:

Now open Monkey.mm and add the following code before the @end marker:

All the above code does is to apply an impulse to make the monkey jump and play the jump sound. We’ll pan the sound as we did for the object sounds in Part One of this tutorial.

Compile and run. Tap the screen to make the monkey jump.

Outta Sight!

But this is far from perfect: if you tap the screen multiple times, the monkey goes through the roof! Worse still, the camera does not follow the monkey.

Let’s fix the camera first. First, add the following code to the top of the update selector, above the existing code, in GameLayer.mm.

The above code copies the monkey’s y-position into a new variable mY. Of course, we could also access the monkey’s ccNode and take the y-coordinate from there. The end result will be the same as multiplying his physics position by the PTM_RATIO.

Now, add these lines to the end of the update selector after the closing curly brace after section #7.

Here, we calculate a good value for the camera’s y-coordinate, one that will more or less center the monkey on the middle of the screen. Clamp the value so that it does not go below 0 so that the camera does not move below ground level.

Now let’s implement parallax scrolling of the background so that the monkey’s movement appears more natural. The effect is quite easy to accomplish: just multiply the background layer by a factor below 1.0 and set the position for the layers. This will make the background layer scroll slower. The further away a layer is from the camera, the smaller the factor must be.

Add this code below the last few lines added to update:

Feel free to adjust the values to your liking.

That’s it – compile and run. Tap the screen multiple times to see the monkey rise. And beware of flying monkeys!!!!

And It All Piles Up!

If you play the game for a while, you’ll notice that the items continue to drop from the same fixed height – sometimes way below the monkey’s current position. In fact, after some time, items don’t even drop because they’ve piled up beyond the spawning point.

Fix this by changing the following line in GameLayer.mm‘s update method:

Change the above line to this:

Items will now spawn 400pt above the monkey’s head, wherever it may be.

Back to the monkey. You’ll have noticed that the monkey can actually jump again and again while up in the air. This just won’t do. We need to fix it so the monkey only jumps when he has contact with the floor.

Let’s start by counting the number of contacts the monkey makes with the floor.

Add a new variable to Monkey.h:

Switch to Monkey.mm and add the following two new collision detection handlers to the end of the file (but above the @end marker):

As the names imply, the first one detects the beginning of a contact/collision with the floor and the second the end of a collision. In the beginContact selector, we increase the value of the floor contact variable, and then decrease it in the endContact selector.

These selectors are going to get called by GBox2D every time a contact starts or ends between the monkey and the floor. (Remember: we created a separate class for the floor so that GBox2D now can call the appropriate selector with the class’s name).

Now, if the monkey is standing on the floor, then the numFloorContacts value should be at least one. Use this to our advantages by wrapping the code in the jump method in Monkey.mm with an if condition to see if the monkey is actually standing on the floor before jumping:

Compile and run. Everything seems fine. Well… except that when the monkey lands on an object, he loses the ability to jump. To fix this, we’re going to consider any contact the monkey has with objects similar to a contact with the floor.

And it’s very simple to implement. Simply add a couple more collision-handling routines to the end of Monkey.mm and count object contacts the same way we counted floor contacts:

Compile and run. Isn’t it much better now? And our game is already playable!

Push It!

Let’s improve the gameplay and allow the monkey to push objects to the left and right – and give him the ability to put his hands above his head to shield him from dropping objects.

Do you remember how you added the sensors to the left and right sides of the monkey in Part One? They come into play now! The key to the sensors was setting up the “Id” parameter in PhysicsEditor. You are going to retrieve this value now!

But before we do that, we need to add a few instance variables to keep count of the left and right sensors, as well as the number of contacts made with the monkey’s head. Add these variables to Monkey.h:

Next, the beginContactWith* and endContactWith* selectors have a contact parameter we can use to determine which part of the monkey has contact with an object – the “Id” value we added in PhysicsEditor is stored as user data in each fixture. So replace the existing object contact handlers for Monkey.mm with the following:

As you see from the new code, you retrieve the Id, which we’ll call fixtureId here, by accessing the contact parameter’s fixture and then accessing the fixture’s user data via the GetUserData method.

Now that we’re tracking the contacts, we can update the monkey’s animation frames to handle additional events.

Here’s the decision table for the various animations:

Using the above table, we modify section #6 of updateCCFromPhysics in Monkey.mm as follows:

Compile and test. Perfect! The monkey now behaves just as we want.

It Takes a Strong Monkey …

Playing a little bit more though, I think the monkey should be a bit stronger under certain conditions. Currently, he’s too weak to break free when an object is above his head. Let’s give him some extra strength when he’s trapped like that and wants to jump.

This is the current line from the jump selector in Monkey.mm that makes the monkey jump:

Replace it with:

That’s our monkey on steroids! He now uses a 2.5-times stronger impulse when there’s an object resting above him – this should allow him to break free of most of the objects.

Let’s also change the walking impulse in case the monkey needs to push an object to the side to break free. Go to updateCCFromPhysics and cut the following lines from section #6:

Now paste that code into section #4 and modify it as follows:

Compile and test – that’s much better. But there’s still a problem: when the monkey slightly grazes an object with his head, the new jump power makes him go through the roof!

We need to clamp his maximum speed inside the updateCCFromPhysics method. Add this to the end of section #3 of the updateCCFromPhysics method:

Notice that in the code above we are directly modifying values controlled by the Box2d engine, thus affecting the overall behaviour of the physics engine. You should try to avoid doing this kind of manipulation.

Compile and test. I like the monkey’s behavior now. He reacts quickly, and is strong enough to push objects but does not become uncontrollable.

Where To Go From Here?

If you don’t have it already, here is all of the source code for this tutorial series.

You’ve now reached the end of Part Two of the MonkeyJump tutorial! The project in its current form is available in the source code zip in the folder called 5-MonkeyJumpAndRun.

Stay tuned for the final part of the series, where we’ll add some performance improvements, add a HUD layer to the game, and yes – kill the monkey! :]

In the meantime, if you have any questions or comments, please join the forum discussion below! (Source: Ray Wenderlich)


上一篇:

下一篇: