如何使用cocos2d制作类似DNF的2D横版格斗过关游戏 PART-2

本文由eseedo翻译。

原译文:http://blog.sina.com.cn/s/blog_4b55f6860101aaav.html

原文参考:http://www.raywenderlich.com/24452/how-to-make-a-side-scrolling-beat-em-up-game-like-scott-pilgrim-with-cocos2d-part-2

欢迎继续之前的横版过关游戏教程系列,这一部分将是该系列的第二部分,同时也是终结篇。

在第一部分的内容里面,我们已经创建了会发呆和挥动拳头的英雄,并且把D-pad方向键摆在了游戏的左下角。

而在这部分的内容中,我们将会学到更多东西,比如添加运动,卷轴滚动,碰撞,敌人,AI,当然还有令人热血沸腾的音乐和音效。

在开始学习这部分的内容之前,请先下载第一部分结束时的项目示例代码(http://cdn1.raywenderlich.com/downloads/PompaDroidPart1.zip)。

同时别忘了下载项目所需的资源文件(http://cdn4.raywenderlich.com/downloads/pd_resources.zip)。

准备好了吗?来吧,让我们干掉这些可恶的androids机器人!哦,不好意思,口误,是droid机器人。

如何使用cocos2d制作类似Scott <wbr>Pilgrim的2D横版格斗过关游戏part2(翻译)



让英雄走两步

在第一部分教程的最后,我们创建了一个D-pad方向键区,并将其显示在屏幕中。但如果此时按下方向键,游戏会直接崩掉。赶紧来修复这个问题吧!

在Xcode中切换到Hero.m,并添加以下代码:

//在

if ((self = [super initWithSpriteFrameName:@"hero_idle_00.png"]))

循环语句中的攻击动作之后添加行走动画:

CCArray*walkFrames=[CCArray arrayWithCapacity:8];
for(i=0; i<8; i++){
CCSpriteFrame*frame =[[CCSpriteFrameCachesharedSpriteFrameCache]spriteFrameByName:[NSStringstringWithFormat:@"hero_walk_d.png", i]];
[walkFramesaddObject:frame];
}
CCAnimation*walkAnimation=[CCAnimationanimationWithSpriteFrames:[walkFramesgetNSArray] delay:1.0/12.0];
self.walkAction=[CCRepeatForeveractionWithAction:[CCAnimateactionWithAnimation:walkAnimation]];

对这种添加动画的代码你应该毫无鸭梨了。这里我们给行走动画添加了新的帧,然后用创建发呆东走类似的方式创建了行走动作。

接下来切换到ActionSprite.m,并添加以下方法:

-(void)walkWithDirection:(CGPoint)direction {
    if (_actionState == kActionStateIdle) {
        [self stopAllActions];
        [self runAction:_walkAction];
        _actionState = kActionStateWalk;
    }
    if (_actionState == kActionStateWalk) {
        _velocity = ccp(direction.x * _walkSpeed, direction.y * _walkSpeed);
        if (_velocity.x >= 0) self.scaleX = 1.0;
        else self.scaleX = -1.0;
    }
}

在这段代码中,我们首先确认前一个动作是否是发呆,然后将动作切换到行走,运行行走动画。如果前一个动作本来就是行走,那么我们秩序根据walkSpeed值来更改精灵的移动速度。

同时这里还检查精灵的左右方向,并通过将scaleX值设置为-1或1来翻转精灵。

当然,为了让英雄的行走动作和D-pad方向键联系在一起,我们必须借助D-pad的代理:GameLayer。

切换到GameLayer.m,并实现以下方法来实现SimpleDPadDelegate的协议:

-(void)simpleDPad:(SimpleDPad *)simpleDPad didChangeDirectionTo:(CGPoint)direction {
    [_hero walkWithDirection:direction];
}
 
-(void)simpleDPadTouchEnded:(SimpleDPad *)simpleDPad {
    if (_hero.actionState == kActionStateWalk) {
        [_hero idle];
    }
}
 
-(void)simpleDPad:(SimpleDPad *)simpleDPad isHoldingDirection:(CGPoint)direction {
    [_hero walkWithDirection:direction];
}

通过以上方法,每当SimpleDPad发送一个方向信息时都会触发英雄的移动方法,而每当SimpleDPad的触摸事件结束时则会触发英雄的发呆方法。

编译运行游戏,然后尝试着用方向键来操控英雄的行走。

如何使用cocos2d制作类似Scott <wbr>Pilgrim的2D横版格斗过关游戏part2(翻译)

好吧,酷哥英雄貌似在移动,但又貌似没有动。走,还是不走,这是个问题。

神马原因呢?让我们再回头看看刚才的walkWithDirection:方法,实际上这里什么也没做,只不过改变了英雄的速度。即便英雄可以达到超音速,宇宙速度,光速,但他如果不走的话,当然只会原地踏步了。

好吧,实际上更改英雄的位置应该由ActionSprite和GameLayer来共同负责。一个ActionSprite精灵永远不知道他现在地图的何处。因此,他并不知道现在已经达到地图的边缘,只知道想走到属于自己的位置。而GameLayer则应负责把他的目标位置转换为实际的位置。

此前我们曾为ActionSprite定义了一个名为desiredPosition的CGPoint类型变量。而这个位置就是ActionSprite应该用到的唯一位置值。

切换到ActionSprite.m,并添加以下方法:

-(void)update:(ccTime)dt {
    if (_actionState == kActionStateWalk) {
        _desiredPosition = ccpAdd(position_, ccpMult(_velocity, dt));
    }
}

以上方法将在游戏刷新屏幕的时候调用,其作用是当英雄处于行走状态时不断更新其目标位置。这里使用了一个简单的计算公式,目标位置= 实际位置+(速度*时间)。理想状态下,不存在加速度的~

注意:这种计算位置的方法被称作欧拉积分法。虽然简单易行,但并不精确。欧拉积分是神马鬼东西?Don’t panic,查查维基百科吧http://en.wikipedia.org/wiki/Euler_integration

当然,考虑到不是在进行物理模拟,欧拉积分法已经够用了。如果对数学无爱,看看就好。

切换到GameLayer.m,并进行以下操作:

//在init方法的if循环中添加以下代码:

 [selfscheduleUpdate];

然后添加以下几个方法:

//add inside if ((self = [super init])) right after [self initTileMap];
[self scheduleUpdate];
 
//add these methods inside the @implementation
-(void)dealloc {
    [self unscheduleUpdate];
}
 
-(void)update:(ccTime)dt {
    [_hero update:dt];
    [self updatePositions];
}
 
-(void)updatePositions {
    float posX = MIN(_tileMap.mapSize.width * _tileMap.tileSize.width - _hero.centerToSides, MAX(_hero.centerToSides, _hero.desiredPosition.x));
    float posY = MIN(3 * _tileMap.tileSize.height + _hero.centerToBottom, MAX(_hero.centerToBottom, _hero.desiredPosition.y));
    _hero.position = ccp(posX, posY);
}

来大概解释下这几个方法的作用:

dealloc方法就不说了,万物有生有灭,有预定消息的方法,就需要取消预定消息的方法。

接下来的update:方法预定了GameLayer类的消息,会在游戏的主循环中实时更新。这里我们首先调用了英雄的消息预定来更新其目标位置,然后调用updatePositions来确定英雄是否在地图地板的边界范围内,并据此来更新实际的位置。

在updatePositions方法中,用到了地图的两个重要属性值:

mapSize:也就是地图中的瓦片数量。总共有10*100个瓦片,但地板上只有3*100个。

tileSize:每个瓦片的实际大小,这里是32*32像素。

这里还用到了ActionSprite的两个测量数值,centerToSides和centerToBottom来确保ActionSprite始终位于屏幕的场景之中。

如果ActionSprite的位置在地图边界内,就会让英雄去他想去的地方。反之如果超出了地图边界,就会让英雄停在当前位置。

注意:这里用到了MIN和MAX这两个函数。MIN函数会比较两个数值,并返回较小的数值,而MAX则会返回较大的数值。Cocos2d中还针对CGPoint提供了一个方便的函数:ccpClamp。

此时编译运行游戏,英雄可以在地图中真正开始探索了。

如何使用cocos2d制作类似Scott <wbr>Pilgrim的2D横版格斗过关游戏part2(翻译)

好吧,走两步,走两步,走着走着你会发现英雄走出了屏幕的右边界,然后不见了。

神马情况?地图的边界在哪里?地图的边界可不是屏幕的边界,作为一个横版卷轴游戏,显然地图应该是可以基于英雄的位置来滚动的。如果你不太清楚怎么弄,可以回过头看看之前的教程(http://blog.sina.com.cn/s/blog_4b55f6860100s9g6.html):

在GameLayer.m中进行下面的操作:

//在update:方法中添加以下代码:

[selfsetViewpointCenter:_hero.position];

//然后在updatePositions方法之后添加以下方法:

//add this in update:(ccTime)dt, right after [self updatePositions];
[self setViewpointCenter:_hero.position];
 
//add this method
-(void)setViewpointCenter:(CGPoint) position {
 
    CGSize winSize = [[CCDirector sharedDirector] winSize];
 
    int x = MAX(position.x, winSize.width / 2);
    int y = MAX(position.y, winSize.height / 2);
    x = MIN(x, (_tileMap.mapSize.width * _tileMap.tileSize.width)
            - winSize.width / 2);
    y = MIN(y, (_tileMap.mapSize.height * _tileMap.tileSize.height)
            - winSize.height/2);
    CGPoint actualPosition = ccp(x, y);
 
    CGPoint centerOfView = ccp(winSize.width/2, winSize.height/2);
    CGPoint viewPoint = ccpSub(centerOfView, actualPosition);
    self.position = viewPoint;
}

以上方法的作用是让英雄始终位于屏幕x轴的中心,当然,英雄在地图边界位置的情况除外。

关于这个方法的详细解释,还是回头看看那篇老教程吧((http://blog.sina.com.cn/s/blog_4b55f6860100s9g6.html))。

编译运行游戏,现在英雄就不会再从你的视野中消失了。

如何使用cocos2d制作类似Scott <wbr>Pilgrim的2D横版格斗过关游戏part2(翻译)

机器人神马的现身吧,爷准备好了

好吧,不要拐杖自由行走可能有点意思,不过英雄独嫌寂寞啊,让爷在空荡荡的走廊里晃来晃去实在是没意思。说好的Androids机器人呢?

好吧,战便战!

之前我们已经创建了精灵的基本模型ActionSprite。我们大可以重用该类来创造由游戏系统控制的角色。

这部分教程会一带而过,因为和创建英雄的代码没有太大区别。

点击command-n来创建一个新类,选择iOS\Cocos2D v2.x\CCNode Class模板,将其设置为ActionSprite的子类,并命名为Robot。

切换到Robot.h,并在文件顶部添加以下代码:

#import"ActionSprite.h"

然后切换到Robot.m,并添加以下方法:

-(id)init {
    if ((self = [super initWithSpriteFrameName:@"robot_idle_00.png"])) {
        int i;
 
        //idle animation
        CCArray *idleFrames = [CCArray arrayWithCapacity:5];
        for (i = 0; i < 5; i++) {
            CCSpriteFrame *frame = [[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName:[NSString stringWithFormat:@"robot_idle_%02d.png", i]];
            [idleFrames addObject:frame];
        }
        CCAnimation *idleAnimation = [CCAnimation animationWithSpriteFrames:[idleFrames getNSArray] delay:1.0/12.0];
        self.idleAction = [CCRepeatForever actionWithAction:[CCAnimate actionWithAnimation:idleAnimation]];
 
        //attack animation
        CCArray *attackFrames = [CCArray arrayWithCapacity:5];
        for (i = 0; i < 5; i++) {
            CCSpriteFrame *frame = [[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName:[NSString stringWithFormat:@"robot_attack_%02d.png", i]];
            [attackFrames addObject:frame];
        }
        CCAnimation *attackAnimation = [CCAnimation animationWithSpriteFrames:[attackFrames getNSArray] delay:1.0/24.0];
        self.attackAction = [CCSequence actions:[CCAnimate actionWithAnimation:attackAnimation], [CCCallFunc actionWithTarget:self selector:@selector(idle)], nil];
 
        //walk animation
        CCArray *walkFrames = [CCArray arrayWithCapacity:6];
        for (i = 0; i < 6; i++) {
            CCSpriteFrame *frame = [[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName:[NSString stringWithFormat:@"robot_walk_%02d.png", i]];
            [walkFrames addObject:frame];
        }
        CCAnimation *walkAnimation = [CCAnimation animationWithSpriteFrames:[walkFrames getNSArray] delay:1.0/12.0];
        self.walkAction = [CCRepeatForever actionWithAction:[CCAnimate actionWithAnimation:walkAnimation]];
 
        self.walkSpeed = 80;
        self.centerToBottom = 39.0;
        self.centerToSides = 29.0;
        self.hitPoints = 100;
        self.damage = 10;
    }
    return self;
}

和英雄一样,以上代码创建了一个具备三种动作状态的机器人:发呆,攻击和行走。我们同时还设置了一些基本属性和测量辅助变量。

当然,你会发现机器人的属性比英雄要低那么一点点。这是理所应当的,如果对方的小弟都比本家大哥牛,爷还怎么混江湖?

现在来添加一堆机器人小弟吧,你已经肾上腺激素分泌过度了,不是吗?

切换到GameLayer.h,并添加以下属性:

@property(nonatomic,strong)CCArray*robots;

然后切换到GameLayer.m,并进行以下操作:

//在文件的顶部添加:

#import "Robot.h"

//然后在init方法中添加一行代码:

[selfinitRobots];

//接着添加initRobots方法的实现代码:

-(void)initRobots {
    int robotCount = 50;
    self.robots = [[CCArray alloc] initWithCapacity:robotCount];
 
    for (int i = 0; i < robotCount; i++) {
        Robot *robot = [Robot node];
        [_actors addChild:robot];
        [_robots addObject:robot];
 
        int minX = SCREEN.width + robot.centerToSides;
        int maxX = _tileMap.mapSize.width * _tileMap.tileSize.width - robot.centerToSides;
        int minY = robot.centerToBottom;
        int maxY = 3 * _tileMap.tileSize.height + robot.centerToBottom;
        robot.scaleX = -1;
        robot.position = ccp(random_range(minX, maxX), random_range(minY, maxY));
        robot.desiredPosition = robot.position;
        [robot idle];
    }
}

以上方法的作用是:

1. 

     创建了包含50个机器人的数组,并将它们添加到精灵表单中。

2. 

     Defines.h中创建的宏定义来随即安放机器人的位置。注意这里将最小的随机数设置为大于屏幕宽度,以免英雄一开始就身陷重围。

3. 

     让每个机器人站着发呆。

编译运行游戏,英雄不再寂寞,一群2B小弟等着你来战。

如何使用cocos2d制作类似Scott <wbr>Pilgrim的2D横版格斗过关游戏part2(翻译)

试着穿行在这些暂时毫无威胁性的机器人中坚,你会发现有点不对,当你走到机器人中间的时候,会发现机器人挡住了英雄的虎躯,这可不对。

为了解决这个问题,我们得明确告诉游戏应首先绘制哪些精灵。很简单,只需要调整z值就好了。

还记得GameLayer.m里面添加角色和背景的代码吗?

//you set the z-value of tileMap lower than actors, so anything drawn in actors appears in front of tileMap
[self addChild:_actors z:-5];     //this is your CCSpriteBatchNode
[self addChild:_tileMap z:-6];    //this is your CCTMXTiledMap

然后来看看我们是如何添加英雄和机器人的:

[_actorsaddChild:_hero];

[_actorsaddChild:robot];

这里有两处区别:

1. 精灵表单和瓦片地图都是直接添加为GameLayer的子节点,而英雄和机器人则是添加为精灵表单的子节点。GameLayer负责以合理的顺序绘制精灵表单和瓦片地图,而精灵表单则应负责以正确的顺序来绘制不同的角色。

2. 

     这里没有明确说明英雄和机器人的z值。但默认情况下,后面添加的精灵对象会有更高的z值。这就是为什么机器人会挡住英雄的虎躯。

为了解决这个问题,我们需要动态处理z值。每当精灵对象沿着屏幕垂直运动的时候,其z值都应改变。精灵在屏幕中的位置越高,其z值就应越低。

在GameLayer.m中进行以下操作:

添加一个新的方法:

//add this method inside the @implementation
-(void)reorderActors {
    ActionSprite *sprite;
    CCARRAY_FOREACH(_actors.children, sprite) {
        [_actors reorderChild:sprite z:(_tileMap.mapSize.height * _tileMap.tileSize.height) - sprite.position.y];
    }
}
 
//add this method inside update, right after [self updatePositions]
[self reorderActors];

然后在update:方法中添加一行代码:

[selfreorderActors];

每次更新精灵的位置时,以上方法都会让精灵表单重新设置每个子节点的z值,其依据是子节点离地图底部的距离。当子节点离地图底部更远时,其z值就会更低。

注意:每个CCNode节点都有一个zOrder属性,但直接修改该属性并不会带来同样的效果。这是因为角色是精灵表单的子节点,因此应该由精灵表单来设置子节点的z值。

编译运行游戏,现在角色的绘制顺序应该是正常的了。

如何使用cocos2d制作类似Scott <wbr>Pilgrim的2D横版格斗过关游戏part2(翻译)

征战四方- 碰撞检测

现在拳击对象也有了,英雄不再寂寞,但如果和他们一起发呆,实在没什么意思。现在该是让机器人吃点苦头的时候了。

为了让英雄可以实际伤害到机器人,我们需要找到一个方法来实现碰撞检测。这里我们会创建一个非常简单的碰撞检测系统。在这个游戏中,我们为每个角色定义了两个矩形/盒子:

1. 

     挨打盒子(hit box):代表精灵的身体

2. 

     攻击盒子(attack box):代表精灵的拳头

如果某个ActionSprite的攻击盒子碰到另一个精灵的挨打盒子,就代表发生了一次亲密的身体接触。当然,我们也能就此知道是谁打了谁。

还记得之前Defines.h中所定义的包围盒吗?

typedefstruct _BoundingBox{

CGRect actual;

CGRect original;

}BoundingBox;

每个包围盒都有两个矩形:实际的,和初始的

1. 初始矩形是每个精灵对象的基本矩形,一旦设置后就不会改变。

2. 

     实际矩形则是在世界空间中的矩形。当精灵移动的时候,该矩形也会跟着一起移动。

切换到ActionSprite.h,并添加以下代码:

@property(nonatomic,assign)BoundingBox hitBox;
@property(nonatomic,assign)BoundingBox attackBox;
 
-(BoundingBox)createBoundingBoxWithOrigin:(CGPoint)origin size:(CGSize)size;

以上代码定义了ActionSprite的两个包围盒:挨打盒,攻击盒。同时还定义了一个工厂方法用于创建包围盒,其作用是根据某个原点位置和矩形大小来创建包围盒。

切换到ActionSprite.m,并添加以下方法:

-(BoundingBox)createBoundingBoxWithOrigin:(CGPoint)origin size:(CGSize)size {
    BoundingBox boundingBox;
    boundingBox.original.origin = origin;
    boundingBox.original.size = size;
    boundingBox.actual.origin = ccpAdd(position_, ccp(boundingBox.original.origin.x, boundingBox.original.origin.y));
    boundingBox.actual.size = size;
    return boundingBox;
}
 
-(void)transformBoxes {
    _hitBox.actual.origin = ccpAdd(position_, ccp(_hitBox.original.origin.x * scaleX_, _hitBox.original.origin.y * scaleY_));
    _hitBox.actual.size = CGSizeMake(_hitBox.original.size.width * scaleX_, _hitBox.original.size.height * scaleY_);
    _attackBox.actual.origin = ccpAdd(position_, ccp(_attackBox.original.origin.x * scaleX_, _attackBox.original.origin.y * scaleY_));
    _attackBox.actual.size = CGSizeMake(_attackBox.original.size.width * scaleX_, _attackBox.original.size.height * scaleY_);
}
 
-(void)setPosition:(CGPoint)position {
    [super setPosition:position];
    [self transformBoxes];
}

其中第一个方法用于创建一个新的包围盒,从而有助于ActionSprite的子类创建属于自己的包围盒。

第二个方法transformBoxes则根据精灵的位置和缩放来更新实际包围盒的远点和大小。这里之所以要用到缩放的概念,是因为scale可以帮助确定精灵的朝向。

切换到Hero.m,并在initf方法的

if ((self = [super initWithSpriteFrameName])

条件语句中创建新的包围盒:

//add inside if ((self = [super initWithSpriteFrameName)) after self.centerToSide
// Create bounding boxes
self.hitBox = [self createBoundingBoxWithOrigin:ccp(-self.centerToSides, -self.centerToBottom) size:CGSizeMake(self.centerToSides * 2, self.centerToBottom * 2)];
self.attackBox = [self createBoundingBoxWithOrigin:ccp(self.centerToSides, -10) size:CGSizeMake(20, 20)];

类似的,切换到Robot.m,并在init方法的

if ((self = [super initWithSpriteFrameName])

条件语句中创建新的包围盒:

//add inside if ((self = [super initWithSpriteFrameName)) after self.centerToSide
// Create bounding boxes
self.hitBox = [self createBoundingBoxWithOrigin:ccp(-self.centerToSides, -self.centerToBottom) size:CGSizeMake(self.centerToSides * 2, self.centerToBottom * 2)];
self.attackBox = [self createBoundingBoxWithOrigin:ccp(self.centerToSides, -5) size:CGSizeMake(25, 20)];

Ok,现在我们已经有了英雄和机器人的挨打盒和攻击盒。其视觉效果如下:

如何使用cocos2d制作类似Scott <wbr>Pilgrim的2D横版格斗过关游戏part2(翻译)

每当某个attack box(红色)碰到hit box(蓝色)时,就代表发生了碰撞。

在编写检查包围盒碰撞的代码前,我们还要确保ActionSprite会对碰撞做出适当的反应。之前我们已经实现了发呆,攻击和行走动作,但还没有创建受伤和死亡的动作。

既然战斗,岂能没有伤痛?!切换到ActionSprite.m,并添加以下方法:

-(void)hurtWithDamage:(float)damage {
    if (_actionState != kActionStateKnockedOut) {
        [self stopAllActions];
        [self runAction:_hurtAction];
        _actionState = kActionStateHurt;
        _hitPoints -= damage;
 
        if (_hitPoints <= 0.0) {
            [self knockout];
        }
    }
}
 
-(void)knockout {
    [self stopAllActions];
    [self runAction:_knockedOutAction];
    _hitPoints = 0.0;
    _actionState = kActionStateKnockedOut;
}

只要精灵对象还没死,那么受到伤害时就会将其状态切换到受伤,然后运行受伤的动画,并从精灵的血条中减去相应的血量。如果血量少于0,那么就会进入死亡状态。

为了完成这两个动作,我们还需要更改Hero和Robot类。

切换到Hero.m,然后在init方法中添加以下代码:

//hurt animation
CCArray *hurtFrames = [CCArray arrayWithCapacity:8];
for (i = 0; i < 3; i++) {
    CCSpriteFrame *frame = [[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName:[NSString stringWithFormat:@"hero_hurt_%02d.png", i]];
    [hurtFrames addObject:frame];
}
CCAnimation *hurtAnimation = [CCAnimation animationWithSpriteFrames:[hurtFrames getNSArray] delay:1.0/12.0];
self.hurtAction = [CCSequence actions:[CCAnimate actionWithAnimation:hurtAnimation], [CCCallFunc actionWithTarget:self selector:@selector(idle)], nil];
 
//knocked out animation
CCArray *knockedOutFrames = [CCArray arrayWithCapacity:5];
for (i = 0; i < 5; i++) {
    CCSpriteFrame *frame = [[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName:[NSString stringWithFormat:@"hero_knockout_%02d.png", i]];
    [knockedOutFrames addObject:frame];
}
CCAnimation *knockedOutAnimation = [CCAnimation animationWithSpriteFrames:[knockedOutFrames getNSArray] delay:1.0/12.0];
self.knockedOutAction = [CCSequence actions:[CCAnimate actionWithAnimation:knockedOutAnimation], [CCBlink actionWithDuration:2.0 blinks:10.0], nil];

然后切换到Robot.m,并在init方法中添加相应的动画代码:

//hurt animation
CCArray *hurtFrames = [CCArray arrayWithCapacity:8];
for (i = 0; i < 3; i++) {
    CCSpriteFrame *frame = [[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName:[NSString stringWithFormat:@"robot_hurt_%02d.png", i]];
    [hurtFrames addObject:frame];
}
CCAnimation *hurtAnimation = [CCAnimation animationWithSpriteFrames:[hurtFrames getNSArray] delay:1.0/12.0];
self.hurtAction = [CCSequence actions:[CCAnimate actionWithAnimation:hurtAnimation], [CCCallFunc actionWithTarget:self selector:@selector(idle)], nil];
 
//knocked out animation
CCArray *knockedOutFrames = [CCArray arrayWithCapacity:5];
for (i = 0; i < 5; i++) {
    CCSpriteFrame *frame = [[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName:[NSString stringWithFormat:@"robot_knockout_%02d.png", i]];
    [knockedOutFrames addObject:frame];
}
CCAnimation *knockedOutAnimation = [CCAnimation animationWithSpriteFrames:[knockedOutFrames getNSArray] delay:1.0/12.0];
self.knockedOutAction = [CCSequence actions:[CCAnimate actionWithAnimation:knockedOutAnimation], [CCBlink actionWithDuration:2.0 blinks:10.0], nil];

这些你应该都不陌生了。我们用创建其它动作类似的方式创建了受伤和死亡动作。当受伤动作结束时会切换到发呆状态,而死亡动作则会让精灵对象在动画后消失。

此时切换到GameLayer.m,并正式添加碰撞检测代码:

//在ccTouchesBegan方法中,在[_hero attack];这行代码后添加以下代码:

//add this inside ccTouchesBegan, right after [_hero attack];
if (_hero.actionState == kActionStateAttack) {
    Robot *robot;
    CCARRAY_FOREACH(_robots, robot) {
        if (robot.actionState != kActionStateKnockedOut) {
            if (fabsf(_hero.position.y - robot.position.y) < 10) {
                if (CGRectIntersectsRect(_hero.attackBox.actual, robot.hitBox.actual)) {
                    [robot hurtWithDamage:_hero.damage];
                }
            }
        }
    }
}

以上代码通过三步来进行碰撞检测:

1. 

     检查英雄的状态是否处于攻击状态,以及机器人是否处于非死亡状态下的其它状态。

2. 

     检查英雄和机器人的位置的y坐标距离在10点以内,这就意味着他们站在同一位置。

3. 

     检查英雄的attack box攻击盒是否和机器人的hit box交汇在一起,如果是,就让机器人受伤。

一旦所有这些条件都符合了,那么当碰撞发生时,就会运行机器人的受伤动作。英雄的伤害值会作为参数传递,这样该方法就知道需要减去多少血。

编译运行游戏,开战吧!

如何使用cocos2d制作类似Scott <wbr>Pilgrim的2D横版格斗过关游戏part2(翻译)

机器人的复仇(简单AI的实现)

爷不断挥拳,所向披靡,但这样似乎一点挑战性都没有?

同时,为了结束一个游戏,还需要游戏的赢输机制。现在你可以轻松单秒地图上的所有机器人,但什么也不会发生。既然是横版过关,那么肯定希望在单秒所有敌人或者被敌人秒了后能结束游戏。

为了让敌人能秒掉英雄(受虐倾向?),你得让机器人学会主动攻击英雄,这就涉及到了简单的AI系统。

这个简单的AI系统基于决策机制而来。在特定的时间间隔里,我们将给每个机器人一次机会来决定接下来该做什么。首先得让他们“活”过来,知道自己该何时做出选择。

切换到Robot.h,并添加以下属性:

@property(nonatomic,assign)doublenextDecisionTime;

然后切换到Robot.m,并在init方法的

if ((self = [super initWithSpriteFrameName]))条件语句中

初始化该属性值。

_nextDecisionTime=0;

然后切换到GameLayer.m,并添加以下方法:

-(void)updateRobots:(ccTime)dt {
    int alive = 0;
    Robot *robot;
    float distanceSQ;
    int randomChoice = 0;
    CCARRAY_FOREACH(_robots, robot) {
        [robot update:dt];
        if (robot.actionState != kActionStateKnockedOut) {
            //1
            alive++;
 
            //2
            if (CURTIME > robot.nextDecisionTime) {
                distanceSQ = ccpDistanceSQ(robot.position, _hero.position);
 
                //3
                if (distanceSQ <= 50 * 50) {
                    robot.nextDecisionTime = CURTIME + frandom_range(0.1, 0.5);
                    randomChoice = random_range(0, 1);
 
                    if (randomChoice == 0) {
                        if (_hero.position.x > robot.position.x) {
                            robot.scaleX = 1.0;
                        } else {
                            robot.scaleX = -1.0;
                        }
 
                        //4
                        [robot attack];
                        if (robot.actionState == kActionStateAttack) {
                            if (fabsf(_hero.position.y - robot.position.y) < 10) {
                                if (CGRectIntersectsRect(_hero.hitBox.actual, robot.attackBox.actual)) {
                                    [_hero hurtWithDamage:robot.damage];
 
                                    //end game checker here
                                }
                            }
                        }
                    } else {
                        [robot idle];
                    }
                } else if (distanceSQ <= SCREEN.width * SCREEN.width) {
                    //5
                    robot.nextDecisionTime = CURTIME + frandom_range(0.5, 1.0);
                    randomChoice = random_range(0, 2);
                    if (randomChoice == 0) {
                        CGPoint moveDirection = ccpNormalize(ccpSub(_hero.position, robot.position));
                        [robot walkWithDirection:moveDirection];
                    } else {
                        [robot idle];
                    }
                }
            }
        }
    }
 
    //end game checker here
}

这段代码看起来有点吓人,不过别担心,我们还是一步步来解释吧。

对于游戏中的每个机器人:

1. 

     我们使用一个变量来保存仍然存活的机器人数量。只要机器人不处于死亡状态,就代表它还活着。该变量将用于判断何时游戏可以结束。

2. 

     判断当前系统时间是否超过了机器人的下次决策时间。如果是,就说明该让机器人做一个新的决策了。CURTIME是我们之前所定义的一个宏。

3. 

     检查机器人离英雄的距离,以判断它是否有机会和英雄取得某种联系。如果是,则让机器人做一个随机决定,让其面向英雄挥拳,或继续沉思。

4. 

     如果机器人决定攻击,则和之前检查英雄的攻击一样进行碰撞检测。只不过这一次挨打的是英雄。

5. 

     如果机器人和英雄的距离小于屏幕宽度,那么机器人就需要决定究竟是走向英雄,还是继续沉思。机器人的运动路线取决于英雄的位置和机器人的位置共同得出的正交向量。

每当机器人做出某个决策后,它的下一次决策时间将设置为未来的某个随机时间点。同时,它还需要继续执行上次决策时间所做出的决策动作。

仍然在GameLayer.m中,进行以下操作:

//在update:方法中添加以下代码:

[selfupdateRobots:dt];

在updatePositions方法中,在

_hero.position = ccp(posX, posY);

这行代码后添加以下代码:

//add inside update, right before [self updatePositions];
[self updateRobots:dt];
 
//add inside the updatePositions method, right after _hero.position = ccp(posX, posY);
// Update robots
Robot *robot;
CCARRAY_FOREACH(_robots, robot) {
    posX = MIN(_tileMap.mapSize.width * _tileMap.tileSize.width - robot.centerToSides, MAX(robot.centerToSides, robot.desiredPosition.x));
    posY = MIN(3 * _tileMap.tileSize.height + robot.centerToBottom, MAX(robot.centerToBottom, robot.desiredPosition.y));
    robot.position = ccp(posX, posY);
}

在以上代码中,我们首先在update:方法中保证游戏循环会调用机器人的AI机制。接下来在updatePositions方法中我们遍历了所有的机器人,并让它们朝目标位置移动。

编译运行游戏,准备迎接机器人的疯狂反扑吧。

如何使用cocos2d制作类似Scott <wbr>Pilgrim的2D横版格斗过关游戏part2(翻译)

继续玩游戏,直到所有的机器人都被消灭,或者英雄被秒掉。这时候游戏就卡住了。如果你看过之前的这篇游戏教程(http://www.raywenderlich.com/14302/how-to-make-a-game-like-fruit-ninja-with-box2d-and-cocos2d-part-1),就该知道我们需要添加一个按钮来重置游戏了。

在GameLayer.m中进行以下操作:

//在文件顶部添加以下代码;

#import"GameScene.h"

然后添加以下方法:

//add to top of file
#import "GameScene.h"
 
//add these methods inside @implementation
-(void)endGame {
    CCLabelTTF *restartLabel = [CCLabelTTF labelWithString:@"RESTART" fontName:@"Arial" fontSize:30];
    CCMenuItemLabel *restartItem = [CCMenuItemLabel itemWithLabel:restartLabel target:self selector:@selector(restartGame)];
    CCMenu *menu = [CCMenu menuWithItems:restartItem, nil];
    menu.position = CENTER;
    menu.tag = 5;
    [_hud addChild:menu z:5];
}
 
-(void)restartGame {
    [[CCDirector sharedDirector] replaceScene:[GameScene node]];
}

endGame方法用于创建一个Restart按钮,当触碰该按钮时就会触发restartGame方法。而restartGame方法的作用很简单,就是重新加载游戏场景。

然后回到updateRobots:方法,在第一个占位符

//end game checker here

处添加以下代码:

if (_hero.actionState == kActionStateKnockedOut && [_hud getChildByTag:5] == nil) {
    [self endGame];
}
 
//然后在第二个占位符//end game checker here处添加以下代码:
if (alive == 0 && [_hud getChildByTag:5] == nil) {
    [self endGame];
}

以上方法的作用都是检查游戏结束条件。第一个是判断英雄是否死亡,如果被秒了,当然就结束了。

而第二个方法的作用是判断还存活的机器人数量是否已经是0了,如果是,当然也结束了。

注意到还有一个判断条件是HudLayer层tag值为5的子节点是否为空,这是因为结束游戏的标签的tag值就是5。这样可以避免游戏陷入死循环。

编译运行游戏,随便发飙吧!

如何使用cocos2d制作类似Scott <wbr>Pilgrim的2D横版格斗过关游戏part2(翻译)

音乐和音效

现在一切就绪,但听不到机器人的凄惨嚎叫似乎满足不了你内心的某种渴望。该是添加一些音乐和音效的时候了!

本示例游戏的背景音乐要感谢来自Incompetech的Kevin Macleod(http://incompetech.com/music/royalty-free/index.html?genre=Electronica),而本人也用bfxr(http://www.bfxr.net/)自己创建了一些音效。

下面开始添加音乐和音效~

首先把资源包里面的Sounds文件夹中的文件拖到项目的Resources中,确保选中opy items into destination group’s folderCreate groups for any added folders

如何使用cocos2d制作类似Scott <wbr>Pilgrim的2D横版格斗过关游戏part2(翻译)

切换到GameLayer.m,并进行以下操作:

//在文件的顶部添加一行代码:
#import "SimpleAudioEngine.h"
 
//然后在init方法的if条件语句中添加以下代码: ((self = [super init])) in init
// Load audio
[[SimpleAudioEngine sharedEngine] preloadBackgroundMusic:@"latin_industries.aifc"];
[[SimpleAudioEngine sharedEngine] playBackgroundMusic:@"latin_industries.aifc"];
[[SimpleAudioEngine sharedEngine] preloadEffect:@"pd_hit0.caf"];
[[SimpleAudioEngine sharedEngine] preloadEffect:@"pd_hit1.caf"];
[[SimpleAudioEngine sharedEngine] preloadEffect:@"pd_herodeath.caf"];
[[SimpleAudioEngine sharedEngine] preloadEffect:@"pd_botdeath.caf"];

然后切换到ActionSprite.m,并进行以下操作:

//在文件顶部添加代码:

#import "SimpleAudioEngine.h"

//然后在hurtWithDamage方法的第一个if条件语句中(在第二个if条件语句前)添加以下代码:
int randomSound = random_range(0, 1);
[[SimpleAudioEngine sharedEngine] playEffect:[NSString stringWithFormat:@"pd_hit%d.caf", randomSound]];

然后切换到Hero.m,并进行以下操作:

//在文件的顶部添加一行代码:

#import "SimpleAudioEngine.h"

//然后添加以下方法:
-(void)knockout {
    [super knockout];
    [[SimpleAudioEngine sharedEngine] playEffect:@"pd_herodeath.caf"];
}

最后切换到Robot.m,并进行以下操作:

//在文件的顶部添加一行代码:

#import "SimpleAudioEngine.h"

//然后添加以下方法:
-(void)knockout {
    [super knockout];
    [[SimpleAudioEngine sharedEngine] playEffect:@"pd_botdeath.caf"];
}

以上代码很简单,就是在特定的场合下播放音乐和音效而已。

OK,编译运行游戏,一切大功告成了!

这里是到目前为止的项目源代码(http://cdn3.raywenderlich.com/downloads/PompaDroidPart2.zip)。

更多好消息:

如果你喜欢2D横版过关游戏,那么可以期待作者的Starter Kit,即将在raywenderlich.com发售。

其中包含了以下更多的内容:

1. 更多动作:3连击,跳跃,奔跑

2. 组合动作:跳跃攻击,奔跑攻击

3. 血条

4. 8向方向键

5. 动画D-pad按钮

6. 状态机应用到游戏事件中,比如敌人蜂拥而出时的战役事件

7. 更多关卡,使用plist文件来设置事件

8. 更多类型的敌人

9. Boss

10. 

     更聪明的敌人AI

11. 

     更好的碰撞检测机制,使用圆形替代矩形

12. 

     根据动作来调整碰撞圆形,并将圆形绘制到屏幕中以方便调试

13. 

     英雄将拥有新的武器:Gauntlet

14. 

     可修建的地图对象

15. 

     可视化的伤害

16. 

     更多更多。。。

期待作者的爆发吧。

标签: cocos2d

?>