GVKun编程网logo

Cocos2D教程:使用SpriteBuilder和Cocos2D 3.x开发横版动作游戏——Part 2(cocos creator横屏)

16

本文将带您了解关于Cocos2D教程:使用SpriteBuilder和Cocos2D3.x开发横版动作游戏——Part2的新内容,同时我们还将为您解释cocoscreator横屏的相关知识,另外,我们

本文将带您了解关于Cocos2D教程:使用SpriteBuilder和Cocos2D 3.x开发横版动作游戏——Part 2的新内容,同时我们还将为您解释cocos creator横屏的相关知识,另外,我们还将为您提供关于cocos2d html5 3.8.1 .修复Cocos2d Particle Builder plist 粒子文件 base64 图片 解压错误、Cocos2d 卡牌塔防 游戏 cocos2d-x游戏开发之旅 第14 15 16 章 源代码调试 注意点 出现无法打开包括文件:“cocos2d.h”: No such file or direct、cocos2d-iphone – cocos2d convertToWorldSpace、cocos2d-iphone – Cocos2d只显示CCSprite的一部分的实用信息。

本文目录一览:

Cocos2D教程:使用SpriteBuilder和Cocos2D 3.x开发横版动作游戏——Part 2(cocos creator横屏)

Cocos2D教程:使用SpriteBuilder和Cocos2D 3.x开发横版动作游戏——Part 2(cocos creator横屏)

本文是“使用Cocos2D 3.x开发横版动作游戏”系列教程的第二篇,同时也是最后一篇。是对How To Make A Side-Scrolling Beat Em Up Game Like Scott Pilgrim with Cocos2D – Part 2的翻译,加上个人理解而成。最重要的是将文中所有代码转换为Cocos2D 3.x版本。众所周知,3.x与2.x的区别非常之大,在触摸机制、渲染机制等方面都与之前版本有了本质的区别。这里将本人摸索的结果加上,供大家参考。

在上一篇教程中,我们已经加载了TiledMap,创建了Hero,并且实现了一个简单地虚拟摇杆。但是我们的虚拟摇杆还无法投入到使用中,无法移动我们的Hero,并且我们并没有加入敌人,这实在不算是一个横版动作游戏。

本篇教程中我将带着大家完成这个游戏,包括实现人物的移动,地图的滚动,碰撞检测、创建敌人、人工智能以及音乐播放。

首先,你要有我们上一篇教程最后写好的项目,也就是我们的第一部分的代码。如果你还没有,你可以从这里下载。

接下来让我们开始吧!

主角动起来!

上一篇教程的最后,如果大家编译并运行项目,点击我们的虚拟摇杆,会发现项目会crash,因为我们还没有实现协议方法。我们马上就会进行实现,不过首先,既然英雄要动起来,那么我们要准备好walk的动作,打开Hero.m,在init方法中添加下面的代码:
//walk action
        NSMutableArray *walkFrames = [NSMutableArray arrayWithCapacity:8];
        for (int i = 0; i < 8; i++) {
            CCSpriteFrame *frame = [[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName:[Nsstring stringWithFormat:@"hero_walk_%02d.png",i]];
            [walkFrames addobject:frame];
        }
        CCAnimation *walkAnimation = [CCAnimation animationWithSpriteFrames:walkFrames delay:1.0 / 12.0f];
        self.walkAction = [CCActionRepeatForever actionWithAction:[CCActionAnimate actionWithAnimation:walkAnimation]];

到现在你应该对这些代码很熟悉了,因为创建所有的动画动作的流程基本上都是一样的:依次创建精灵帧并保存->用这些精灵帧生成动画对象->使用动画对象生成动作对象->赋值给相关属性。

接下来,找到Actionsprite.m,添加下面的方法
- (void)walkWithDirection:(CGPoint)direction {
    if (self.state == kActionStateIdle) {
        [self stopAllActions];
        [self runAction:self.walkAction];
        self.state = kActionStateWalk;
    }
    
    if (self.state == kActionStateWalk) {
        self.veLocity = ccp(direction.x * self.walkSpeed,direction.y * self.walkSpeed);
        if (self.veLocity.x >= 0) {
            self.scaleX = 1.0;
        } else {
            self.scaleX = -1.0;
        }
    }
}

这里检测之前角色的状态,规定只有当角色是idle状态时才可以切换到walk状态。然后让该角色执行创建好的walk的动作,接下来,根据传入进来的参数——角色移动的方向来决定角色的“朝向”,也就是scaleX属性(默认朝右)。之前提到过,direction是一个点,横坐标的正负决定着角色x轴的移向,纵坐标的正负决定着角色y轴的移向。

准备工作完成之后,我们就可以来实现之前没有实现的三个协议方法了。
找到GameLayer.m,在最后添加这三个方法的实现:
#pragma mark - SimpleDPadDelegate

- (void)simpleDPad:(SimpleDPad *)dPad didChangeDirectionTo:(CGPoint)direction {
    [self.hero walkWithDirection:direction];
}

- (void)simpleDPad:(SimpleDPad *)dPad isHoldingDirection:(CGPoint)direction {
    [self.hero walkWithDirection:direction];
}

- (void)simpleDPadTouchEnded:(SimpleDPad *)dPad {
    if (self.hero.state == kActionStateWalk) {
        [self.hero idle];
    }
}

每当你按下、移动我们的虚拟摇杆时就会触发主角的walkWithDirection:方法,而当你抬起手指时,主角又会进入idle状态并执行idle的动作。
现在编译然后运行app,按下虚拟摇杆移动主角。这次不会再crash了!

不过等一下- -我们的英雄好像只能原地踏步,并没有实际移动。为什么会这样?
让我们回到刚才的walkWithDirection:方法,仔细看一下,会发现这个方法里只不过是改变了角色的veLocity属性,并没有实际上改变角色的位置。因此我们还需要“实时”更新角色的位置。
分析一下这里的实现思路:Actionsprite类需要移动,但是他不会知道自己在地图中的具体位置,因此他永远不知道何时走到地图的边缘,何时与地图上的障碍物碰撞……。他所知道的就只有自己的“期望”位置——由用户操纵虚拟摇杆决定的下一步该去的位置。因此我们需要在GameLayer中将这个“期望”位置加以判断并切换为“真正”位置。所以说,移动的实现要同时用到这两个类。
之前在Actionsprite里我们已经定义了一个CGPoint类型的属性——desiredPosition。这个就表示角色的“期望”位置,现在我们切到Actionsprite.m中,添加下面的方法:
#pragma mark - Schedule

- (void)update:(CCTime)delta {
    if (self.state == kActionStateWalk) {
        self.desiredPosition = ccpAdd(self.position,ccpMult(self.veLocity,delta));
    }
}

这个方法干了什么呢?首先判断状态,然后将角色的速度(有正负)乘以时间(得到距离),加上当前的位置对应坐标,得到的就是角色的“期望”位置。
注意:在3.x中,我们不需要显示调用[self scheduleUpdate];了,当我们重写了CCNode的update:方法时,Cocos2D会自动为我们每帧调用这里的方法。因此,此时我们实现了Hero的“期望”位置更新逻辑之后,就不需要在GameLayer的update中调用该方法了(当然,调用了也没有事,而且可能逻辑更清晰一些),这是与原教程的另一个不同之处。
接下来切换到GameLayer.m,添加如下代码:
#pragma mark - Schedule

- (void)update:(CCTime)delta {
    [self updatePosition];
}

- (void)updatePosition {
    float posX = MIN(self.tileMap.tileSize.width * self.tileMap.mapSize.width - self.hero.centerToSides,MAX(self.hero.centerToSides,self.hero.desiredPosition.x));
    float posY = MIN(ROAD_MAP_SIZE * self.tileMap.tileSize.height + self.hero.centerToBottom,MAX(self.hero.centerToBottom,self.hero.desiredPosition.y));
    
    self.hero.position = ccp(posX,posY);
}

#pragma mark - EndGame

- (void)dealloc {
    [self unscheduleAllSelectors];
}

然后在顶部添加宏定义
#define ROAD_MAP_SIZE 3
在这段代码中,你设置了一个定时器,不断刷新主角的位置。主角的位置并不是简单地设置为“期望”位置,因为主角不能跑到地图之外。拿x方向来说,这里用了centerToSides作为最小值,地图横线距离减去centerToSides作为最大值,规定主角在这个范围之间移动。Cocos2D中有一个宏ccclamp可以实现同样地功能。
现在编译并运行,你会看到,你的主角真正动起来了,而且碰到左侧边界以及上下边界时都会停下来。


但是,这里却有另一个问题,那就是如果主角一直往右走,很快就从屏幕上消失了,也就是说,地图的视心无法随着主角移动。
这里就需要一点数学知识了,在GameLayer.m中“Schedlue”部分添加该方法:
- (void)updateViewPointCenter:(CGPoint)point {
    float x = MAX(VISIBLE_SIZE.width / 2,point.x);
    float y = MAX(VISIBLE_SIZE.height / 2,point.y);
    
    x = MIN(x,(self.tileMap.mapSize.width * self.tileMap.tileSize.width) - VISIBLE_SIZE.width / 2);
    y = MIN(y,(self.tileMap.mapSize.height * self.tileMap.tileSize.height) - VISIBLE_SIZE.height / 2);
    CGPoint actualPoint = ccp(x,y);
    
    CGPoint centerPoint = ccp(VISIBLE_SIZE.width / 2,VISIBLE_SIZE.height / 2);
    CGPoint viewPoint = ccpsub(centerPoint,actualPoint);
    
    self.position = viewPoint;
}

接着在update:方法中调用该方法:
//在update:方法中添加下面这一行
[self updateViewPointCenter:self.hero.position];
这个方法比较三个点的x、y的大小:传入点,当前视中心点,到达最右边缘后视中心点。初始状态下“当前视中心点”也可以看做“到达最左边缘后视中心点”。也就是说,当传入点在左右边缘范围内移动时,视中心点随着该点的移动而不断切换。而到达两侧边缘后,视中心点固定不变。这样就达到了随着传入点(主角的位置)移动视图的效果。另外,我们这里与传统纵版射击类游戏不同的是,很多纵版射击类游戏通过两张背景拼接、移动Sprite来达到移动背景的效果。而这里移动的却是layer,也就是self.position。
不要忘了在update中调用该方法。
现在编译并运行,这次你真的可以随意移动你的主角了。


敌人出现!

能够随意走动当然很有趣,不过如果仅仅如此那么这个游戏未免太无聊了些。是时候让我们英雄的敌人出现了。
接下来的步伐会快一些了,因为我们已经有了角色的基类Actionsprite,有了Hero类的经验,你会在敌人Robot类中发现很多熟悉的面孔。
command+N新建一个类Robot,继承自Actionsprite。在Robot.m中添加如下代码:
- (id)init {
    self = [super initWithSpriteFrame:[[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName:@"robot_idle_00.png"]];
    if (self) {
        
        //idle
        NSMutableArray *idleFrame = [NSMutableArray arrayWithCapacity:5];
        for (int i = 0; i < 5; i++) {
            CCSpriteFrame *frame = [[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName:[Nsstring stringWithFormat:@"robot_idle_%02d.png",i]];
            [idleFrame addobject:frame];
        }
        CCAnimation *idleAnimation = [CCAnimation animationWithSpriteFrames:idleFrame delay:1.0 / 12.0f];
        self.idleAction = [CCActionRepeatForever actionWithAction:[CCActionAnimate actionWithAnimation:idleAnimation]];
        
        //attack
        NSMutableArray *attackFrame = [NSMutableArray arrayWithCapacity:5];
        for (int i = 0; i < 5; i++) {
            CCSpriteFrame *frame = [[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName:[Nsstring stringWithFormat:@"robot_attack_%02d.png",i]];
            [attackFrame addobject:frame];
        }
        CCAnimation *attackAnimation = [CCAnimation animationWithSpriteFrames:attackFrame delay:1.0 / 24.0f];
        self.attackAction = [CCActionSequence actionOne:[CCActionAnimate actionWithAnimation:attackAnimation] two:[CCActionCallFunc actionWithTarget:self selector:@selector(idle)]];
        
        //walk
        NSMutableArray *walkFrame = [NSMutableArray arrayWithCapacity:6];
        for (int i = 0; i < 6; i++) {
            CCSpriteFrame *frame = [[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName:[Nsstring stringWithFormat:@"robot_walk_%02d.png",i]];
            [walkFrame addobject:frame];
        }
        CCAnimation *walkAnimation = [CCAnimation animationWithSpriteFrames:walkFrame delay:1.0 / 12.0f];
        self.walkAction = [CCActionRepeatForever actionWithAction:[CCActionAnimate actionWithAnimation:walkAnimation]];
        
        self.walkSpeed = 80;
        self.centerToBottom = 39.0f;
        self.centerToSides = 29.0f;
        self.hitPoints = 100;
        self.damage = 10;
    }
    
    return self;
}

很熟悉不是吗?创建动画、创建动作、为属性赋值……
接下来,让我们为游戏添加一些Robot,打开GameLayer.h,添加属性声明:
@property (strong,nonatomic) NSMutableArray *robots;

然后打开GameLayer.m,引入Robot.h头文件,添加initRobots方法并在init中调用,initRobots方法实现如下:
- (void)initRobots {
    int robotsNumber = 50;
    self.robots = [NSMutableArray arrayWithCapacity:robotsNumber];
    
    Robot *temp = [Robot node];
    int minX = VISIBLE_SIZE.width + temp.centerToSides;
    int minY = temp.centerToBottom;
    int maxX = self.tileMap.mapSize.width * self.tileMap.tileSize.width - temp.centerToSides;
    int maxY = ROAD_MAP_SIZE * self.tileMap.tileSize.height + temp.centerToBottom;
    
    for (int i = 0; i < robotsNumber; i++) {
        Robot *robot = [Robot node];
        [self addChild:robot z:-5];
        [self.robots addobject:robot];
        
        [robot setPosition:ccp(RANDOM_RANGE(minX,maxX),RANDOM_RANGE(minY,maxY))];
        robot.desiredPosition = robot.position;
        robot.scaleX = -1;
        
        [robot idle];
    }
}

这里,你指定了机器人总数,并通过我们之前定义的宏来创建随机数,用来决定Robots的位置,然后将这些robots保存起来并添加到地图上去。
编译并运行,你就能看到成群的机器人出现在地图上了。

不过还是有个小问题, 你会发现我们的主角被robots给遮挡住了,按照常理来说,精灵之间的遮挡关系应该是这样的:在地图的稍“下”方的精灵来遮挡在地图稍“上”方的精灵。也就是说,上面那副图片,应该是英雄遮挡机器人才对。
谈到遮挡,就自然会想到精灵的绘制顺序,也就是z属性。该属性越高,精灵越晚被绘制,精灵就会出现在越上方(这里的上是指遮挡关系中的上)。按照上面的需求,我们需要动态地改变精灵的z属性来实现该功能。但是你会发现如果你直接设置精灵的z属性是没有用的,这里你需要的是一个方法reorderChild:z:。
在GameLayer.m中“Schedule”部分添加方法reorderActors并在scheduleUpdate:方法中调用,其实现如下:
- (void)reorderActors {
    for (CCNode *sprite in self.children) {
        if ([sprite isKindOfClass:[Actionsprite class]]) {
            [self reorderChild:sprite z:self.tileMap.mapSize.height * self.tileMap.tileSize.height - sprite.position.y];
        }
    }
}

注意在3.x中reorder方法需要导入头文件:
#import "CCNode_Private.h"


这里根据精灵在地图上的y轴的位置来动态调整精灵的z轴,这样就不会出现上述问题了,现在编译运行,你就可以看到正确的遮挡关系了:

碰撞检测

接下来我们要解决一个很重要的问题:攻击与被攻击的检测。还记得之前在Define.h中定义了一个结构体BoundingBox吗:
//struct
typedef struct BoundingBox {
    CGRect actual;
    CGRect original;
} BoundingBox;
这个结构体是干什么用的呢?它定义了两个CGRect类型的变量。original用来表示精灵的初始化状态下的坐标与大小,由于精灵(主角和Robot)是会动的,所以我们需要一个actual来记录某一时刻精灵的实际位置与大小(大小往往不变,变的是方向,后文会有详述)。
有了这个结构体,我们就可以用actual来做碰撞检测了。因此我们要先完善这个结构体相关的代码。打开Actionsprite.h,添加如下代码:
//boundingBox
@property (assign,nonatomic) BoundingBox hitBox;
@property (assign,nonatomic) BoundingBox attackBox;

//create bounding Box
- (BoundingBox)createBoundingBoxWithOrigin:(CGPoint)origin andSize:(CGSize)size;

提供一个方法用来创建BoundingBox。然后在Actionsprite.m中实现:
#pragma mark - BoundingBox

- (BoundingBox)createBoundingBoxWithOrigin:(CGPoint)origin andSize:(CGSize)size {
    BoundingBox Box;
    
    Box.original.origin = origin;
    Box.original.size = size;
    
    Box.actual.origin = ccpAdd(self.position,ccp(origin.x,origin.y));
    Box.actual.size = size;
    
    return Box;
}

- (void)transformBoundingBox {
    //to consider the direction of sprite,we should use scale
    //onle _xxx Could be assinged,self.xxx is not assingable
    _hitBox.actual.origin = ccpAdd(self.position,ccp(self.hitBox.original.origin.x * self.scaleX,self.hitBox.original.origin.y * self.scaleY));
    _hitBox.actual.size = CGSizeMake(self.hitBox.original.size.width * self.scaleX,self.hitBox.original.size.height * self.scaleY);
    
    _attackBox.actual.origin = ccpAdd(self.position,ccp(self.attackBox.original.origin.x * self.scaleX,self.attackBox.original.origin.y * self.scaleY));
    _attackBox.actual.size = CGSizeMake(self.attackBox.original.size.width * self.scaleX,self.attackBox.original.size.height * self.scaleY);
}

#pragma mark - Setter

- (void)setPosition:(CGPoint)position {
    [super setPosition:position];
    
    [self transformBoundingBox];
}

第一个方法中根据传入的CGPoint和CGSize创建BoundingBox,此时actual和original的区别是一个position。
第二个方法是用来更新BoundingBox的,根据角色当前的位置和朝向(scale)来改变actual,因为original在设立好之后就是不变的了。其作用就是更新actual用。
接下来的setter表示每次更新角色的位置时同时更新BoundingBox。

我知道你可能会对hitBox和attackBox有疑惑,不过我们先把代码写上,写完了我会给出解释的。
打开Hero.m,在init方法中添加对自己的hitBox和attackBox的定义:
//based on center of sprite
        self.hitBox = [self createBoundingBoxWithOrigin:ccp(-self.centerToSides,-self.centerToBottom) andSize:CGSizeMake(self.centerToSides * 2,self.centerToBottom * 2)];
        self.attackBox = [self createBoundingBoxWithOrigin:ccp(self.centerToSides,-10) andSize:CGSizeMake(20,20)];

同样地,打开Robot.m, 在init方法中添加对自己的hitBox和attackBox的定义:
         //based on center of sprite
         self.hitBox = [self createBoundingBoxWithOrigin:ccp(-self.centerToSides,-5) andSize:CGSizeMake(25,20)];

现在,我们来解释一下这两个Box分别是什么作用。玩游戏的时候,我们会有一个常识——角色会“出拳”(近战)来攻击敌人,只有当出的拳打中敌人的身体时才算击中。这就是attackBox和hitBox。下面有一副图清晰地解释了原因:

之前将状态机的时候为角色定义了五种状态:平常、攻击、走路、受伤、死亡。我们已经实现了三种,现在还差受伤和死亡状态,接下来马上就会用到了,所以我们现在给出实现。
打开Actionsprite.m,添加下面两个方法:
- (void)hurtWithdamage:(CGFloat)damage {
    if (self.state != kActionStateKnockedOut) {
        [self stopAllActions];
        [self runAction:self.hurtAction];
        
        self.state = kActionStateHurt;
        
        self.hitPoints -= damage;
        if (self.hitPoints <= 0) {
            [self knockOut];
        }
    }
}

- (void)knockOut {
    [self stopAllActions];
    [self runAction:self.knockedOutAction];
    
    self.hitPoints = 0;
    self.state = kActionStateKnockedOut;
}

只要角色没有死亡,被攻击时就会切换到受伤状态,执行动画动作,然后判断生命值,若HP低于0,则变成死亡状态。
当然,为了完成这些动画,我们需要为主角和Robot分别定义这两个动作。又是老朋友了:]
打开Hero.m,在init方法中定义walk动作之后添加代码:
        //hurt action
        NSMutableArray *hurtFrames = [NSMutableArray arrayWithCapacity:3];
        for (int 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 delay:1.0 / 12.0f];
        self.hurtAction = [CCActionSequence actionOne:[CCActionAnimate actionWithAnimation:hurtAnimation] two:[CCActionCallFunc actionWithTarget:self selector:@selector(idle)]];
        
        //knock out action
        NSMutableArray *knockOutFrames = [NSMutableArray arrayWithCapacity:5];
        for (int i = 0; i < 5; i++) {
            CCSpriteFrame *frame = [[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName:[Nsstring stringWithFormat:@"hero_knockout_%02d.png",i]];
            [knockOutFrames addobject:frame];
        }
        CCAnimation *knockOutAnimation = [CCAnimation animationWithSpriteFrames:knockOutFrames delay:1.0 / 12.0f];
        self.knockedOutAction = [CCActionSequence actionOne:[CCActionAnimate actionWithAnimation:knockOutAnimation] two:[CCActionBlink actionWithDuration:2.0f blinks:10.0f]];
类似地,打开Robot.m,在同样地位置添加:

        //hurt action
        NSMutableArray *hurtFrames = [NSMutableArray arrayWithCapacity:3];
        for (int 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 delay:1.0 / 12.0f];
        self.hurtAction = [CCActionSequence actionOne:[CCActionAnimate actionWithAnimation:hurtAnimation] two:[CCActionCallFunc actionWithTarget:self selector:@selector(idle)]];
        
        //knock out action
        NSMutableArray *knockOutFrames = [NSMutableArray arrayWithCapacity:5];
        for (int i = 0; i < 5; i++) {
            CCSpriteFrame *frame = [[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName:[Nsstring stringWithFormat:@"robot_knockout_%02d.png",i]];
            [knockOutFrames addobject:frame];
        }
        CCAnimation *knockOutAnimation = [CCAnimation animationWithSpriteFrames:knockOutFrames delay:1.0 / 12.0f];
        self.knockedOutAction = [CCActionSequence actionOne:[CCActionAnimate actionWithAnimation:knockOutAnimation] two:[CCActionBlink actionWithDuration:2.0f blinks:10.0f]];

这里规定,受伤后立刻恢复为idle状态,死亡后闪烁两秒。
一切都准备就绪,最后一步:碰撞检测。我们有了要检测的东西BoundingBox,有了检测成功后的执行动作,现在,在GameLayer.m的touchBegan方法中添加下面这段代码:
//添加到touchBegan的[self.hero attack]后
//collision detection
    if (self.hero.state == kActionStateAttack) {
        for (Robot *robot in self.robots) {
            if (robot.state != kActionStateKnockedOut) {
                if (fabsf(robot.position.y - self.hero.position.y) < 10) {
                    if (CGRectIntersectsRect(robot.hitBox.actual,self.hero.attackBox.actual)) {
                        [robot hurtWithdamage:self.hero.damage];
                    }
                }
            }
        }
    }

这一小段代码做了三件事:
1、状态检测,确保英雄的状态是attack,Robot的状态不是死亡。
2、范围检测,确保英雄和Robot的y轴间隔不会太大。
3、矩形相交检测,也就是简单地碰撞检测了。
如果上述三个条件均满足,那么就判定为英雄攻击到了Robot,那么Robot就应该执行相关的动作和逻辑了。
现在编译并运行,享受战斗吧!


简单的AI与决策树

你一直打,一直打,最后把所有的机器人都打到了,然后。。就没有然后了。这样有什么意思呢?所谓游戏就是要输赢共存。如果这些机器人都是扮演沙袋的角色,那么这游戏也不会有人玩的。我们要让机器人攻击英雄!这就要用到AI(Artificial Intelligence)的相关知识了。
首先简单介绍一下决策树的知识,树型结构,一个父结点,多个子节点。决策树,指某一时刻某一状态下有多种选择,通过某种算法来确定下一步的选择。我们的AI就依据与此。由Robot自己去判断,当距离主角较远时是朝主角移动还是静止,当靠近主角时是攻击主角还是“挠一挠头”。
打开Robot.h,添加属性声明:
@property (assign,nonatomic) double nextDecisionTime;
在Robot.m中的init方法中进行初始化:
self.nextDecisionTime = 0;
见名知意,这个属性表示下一次决策时间。
接下来应该是本篇教程中最麻烦的一个方法了,别着急,其实也是很好理解的。
在GameLayer.m中的“Schedule“部分添加方法实现:
<span>- (void)updateRobots:(CCTime)delta {
    int alive = 0;
    int randomChoice = 0;
    float distanceSQ = 0;
    
    for (Robot *robot in self.robots) {
        [robot update:delta];
        
        
        if (robot.state != kActionStateKnockedOut) {
            alive++;
            
            if (CURRENT_TIME > robot.nextDecisionTime) {
                distanceSQ = ccpdistanceSQ(robot.position,self.hero.position);
                
                
                if (distanceSQ <= 50 * 50) {
                    robot.nextDecisionTime = CURRENT_TIME + FLOAT_RANDOM_RANGE(0.1,0.5);
                    randomChoice = RANDOM_RANGE(0,1);
                    
                    if (randomChoice == 0) {
                        
                        if (self.position.x >= robot.position.x) {
                            robot.scaleX = 1.0;
                        } else {
                            robot.scaleX = -1.0;
                        }
                        
                        //attack and collision detection
                        [robot attack];
                        if (robot.state == kActionStateAttack) {
                            if (self.hero.state != kActionStateKnockedOut) {
                                if (fabsf(robot.position.y - self.hero.position.y) < 10) {
                                    if (CGRectIntersectsRect(robot.attackBox.actual,self.hero.hitBox.actual)) {
                                        [self.hero hurtWithdamage:robot.damage];
                                        }
                                    }
                                }
                            }
                        }
                        
                        
                    } else {
                        [robot idle];
                    }
                    
                } else if (distanceSQ <= VISIBLE_SIZE.width * VISIBLE_SIZE.width) {
                    robot.nextDecisionTime = CURRENT_TIME + FLOAT_RANDOM_RANGE(0.5,1.0);
                    randomChoice = RANDOM_RANGE(0,2);
                    if (randomChoice == 0) {
                        //move
                        CGPoint direction = ccpnormalize(ccpsub(self.hero.position,robot.position));
                        [robot walkWithDirection:direction];
                    } else {
                        [robot idle];
                    }
                }   //distance
                
                
                
            }   //if (CURRENT_TIME > robot.nextDecisionTime)
            
        }   //if (robot.state != kActionStateKnockedOut)
        
    }   //foreach
    
}</span>

然后,在update:方法中添加调用:
[self updateRobots:delta];


让我们来慢慢看看这一段代码做了哪些事:
1、通过一个变量alive保存存货的Robot的数量,当一个Robot的状态为knockedOut时判定为死亡,该变量用于随后判断输赢。
2、检查当前时间是否达到Robot的下一个相应决策时间。如果达到了,就说明该Robot需要做一个新的决定了。这里用到了我们之前定义的宏。
3、检查Robot与英雄之间的距离,这决定着Robot的可选决策。从这里开始分支,如果距离过远(超过屏幕距离)则不需要让其作任何选择(也就是说保持idle状态即可)。如果距离稍远,就通过随机数决定是朝主角方向移动还是静止不动。如果距离足够近,那么依然通过随机数决定是否出拳攻击。
4、如果Robot决定攻击,那么与之前在检测英雄攻击Robot一样,作Robot攻击英雄的碰撞检测。
5、注意到Robot的移动方向不再是规定好-1、1数值的CGPoint了,而是数值随机的单位向量。
每次Robot做出一个新的决策时,重置其nextDecisionTime为当前时间+一随机数值。为了能让Robot在随后做出下一次决策。

最后,在updatePosition方法中添加对Robot的位置的更新代码:
//在updatePosition方法中添加
    for (Robot *robot in self.robots) {
        float robotPosX = MIN(self.tileMap.tileSize.width * self.tileMap.mapSize.width - robot.centerToSides,MAX(robot.centerToSides,robot.desiredPosition.x));
        float robotPosY = MIN(ROAD_MAP_SIZE * self.tileMap.tileSize.height + robot.centerToBottom,MAX(robot.centerToBottom,robot.desiredPosition.y));
        robot.position = ccp(robotPosX,robotPosY);
    }


终于完成了~~现在,编译并运行,感受一下机器人大军的厉害吧:]


现在,我们的游戏已经有胜有负了,是时候为其添加胜负界面了,这里界面部分大家就可以自己写了,我就只是简单地展示一段文字即可
在GameLayer.m中添加方法:
- (void)endGameWithResult:(GameResult)result {
    Nsstring *label = nil;
    if (result == kGameResultLost) {
        label = @"Game Over";
    } else if (result == kGameResultWin) {
        label = @"You Win!";
    }
    
    CCButton *restartBtn = [CCButton buttonWithTitle:label fontName:@"Arial" fontSize:30];
    [restartBtn setPosition:CENTER];
    [restartBtn setTarget:self selector:@selector(restartGame)];
    restartBtn.name = @"restart";
    [self.scene addChild:restartBtn z:500];
}

- (void)restartGame {
    CCTransition *trans = [CCTransition transitionFadeWithDuration:0.4f];
    trans.outgoingSceneAnimated = YES;
    trans.incomingSceneAnimated = YES;
    [[CCDirector sharedDirector] replaceScene:[GameScene node] withTransition:trans];
}
其中GameResult是自己定义的枚举变量,其值也都在if-else块中体现出来了。

关键是在什么时候调用呢?
我们知道,胜利的时机是所有机器人都被打败的时候,也就是之前那个用于计算活着的机器人数量的变量alive为0的时候。失败的时机是主角的HP为0的时候,也就是说处理胜负的逻辑都体现在updateRobots:方法中。
在updateRobots相应位置添加代码:
//添加到updateRobots方法的最下端,最外层循环之外
//check game win
    if (alive == 0 && [self getChildByName:@"restart" recursively:YES] == nil) {
        [self endGameWithResult:kGameResultWin];
    }

//添加到[self.hero hurtWithdamage:robot.damage];之后
//check game over
    if (self.hero.state == kActionStateKnockedOut && [self getChildByName:@"restart" recursively:YES] == nil) {
        [self endGameWithResult:kGameResultLost];
    }
和这群机器人玩得愉快!



锦上添花:游戏音效

这么热血沸腾的游戏这么能不配有一个燃曲呢?感谢作者@Allen Tan为我们提供的音效素材(当然还有本系列教程以及一切资源)。
将从上一篇教程中下载下来的资源文件夹中Sounds文件夹拖入项目,勾选上copy items if needed。
注意:3.x中Cocos2D已经不用SimpleAudioEngine来处理音乐了,而是采用OpenAL,对应的类名为OALSimpleAudio,对于我们使用来说大同小异。同样的单例、同样的prepare,同样的play……

打开GameLayer.m,添加方法initMusics并在init中调用,initMusics方法的实现:
- (void)initMusics {
    [[OALSimpleAudio sharedInstance] preloadBg:@"latin_industries.aifc"];
    [[OALSimpleAudio sharedInstance] playBg:@"latin_industries.aifc" loop:YES];
    
    OALSimpleAudio *audio = [OALSimpleAudio sharedInstance];
    [audio preloadEffect:@"pd_botdeath.caf"];
    [audio preloadEffect:@"pd_herodeath.caf"];
    [audio preloadEffect:@"pd_hit0.caf"];
    [audio preloadEffect:@"pd_hit1.caf"];
}

打开Actionsprite.m,在hurtWithdamage:方法中添加:
        int randomSound = RANDOM_RANGE(0,1);
        [[OALSimpleAudio sharedInstance] playEffect:[Nsstring stringWithFormat:@"pd_hit%d.caf",randomSound]];

打开Hero.m,重写knockOut方法:
- (void)knockOut {
    [super knockOut];
    
    [[OALSimpleAudio sharedInstance] playEffect:@"pd_herodeath.caf"];
}

打开Robot.m,同上:
- (void)knockOut {
    [super knockOut];
    
    [[OALSimpleAudio sharedInstance] playEffect:@"pd_botdeath.caf"];
}

一切完成!!现在编译并运行,享受你自己写的完整的横版动作游戏吧

何去何从?

恭喜你,你亲手写出了一个非常有意思的横版动作游戏。完成了包括人物与地图的移动、攻击与被攻击的检测、人工智能与决策树、音乐播放等功能的实现。
你可以从这里获取到我们工程的完整的源代码与所有资源。 再次感谢本文的作者 @Allen Tan。
虽然本教程已经结束了,但你能做的还有很多,一块真正可玩的横版动作游戏还要包括很多功能我们没有实现,例如Boss系统、装备系统、任务系统…………所以,永远不要停下你前进的步伐!
As usual,如果你对本教程有任何疑问或者建议,请在下方留下你的评论。

cocos2d html5 3.8.1 .修复Cocos2d Particle Builder plist 粒子文件 base64 图片 解压错误

cocos2d html5 3.8.1 .修复Cocos2d Particle Builder plist 粒子文件 base64 图片 解压错误

plist 粒子是 Cocos2d Particle Builder 生成的

cocos2dx 版本 3.8.1


路径
frameworks\cocos2d-html5\cocos2d\compression\ZipUtils.js

文件名称

ZipUtils.js.js

已修改 13

删除 1


以下是补丁.


diff --git a/frameworks/cocos2d-html5/cocos2d/compression/ZipUtils.js b/frameworks/cocos2d-html5/cocos2d/compression/ZipUtils.js
index ccecd63..8288b98 100644
--- a/frameworks/cocos2d-html5/cocos2d/compression/ZipUtils.js
+++ b/frameworks/cocos2d-html5/cocos2d/compression/ZipUtils.js
@@ -25,7 +25,19 @@ cc.unzip = function () {
 */
cc.unzipBase64 = function () {
  var tmpInput = cc.Codec.Base64.decode.apply(cc.Codec.Base64,arguments);
-  return  cc.Codec.GZip.gunzip.apply(cc.Codec.GZip,[tmpInput]);
+  var ret;
+  try{
+    ret = cc.Codec.GZip.gunzip.apply(cc.Codec.GZip,[tmpInput]);
+  }
+  catch(e){
+    console.log('unzip base64 error try again');
+    if(tmpInput.length>8){
+      tmpInput = tmpInput.slice(7,tmpInput.length);
+      ret = tmpInput;
+    }
+  }
+
+  return ret;
};

/**



希望有帮助

Cocos2d 卡牌塔防 游戏 cocos2d-x游戏开发之旅 第14 15 16 章 源代码调试 注意点 出现无法打开包括文件:“cocos2d.h”: No such file or direct

Cocos2d 卡牌塔防 游戏 cocos2d-x游戏开发之旅 第14 15 16 章 源代码调试 注意点 出现无法打开包括文件:“cocos2d.h”: No such file or direct

Cocos2d 卡牌塔防 游戏 cocos2d-x游戏开发之旅 第14 15 16 章 源代码调试 注意点

出现无法打开包括文件:“cocos2d.h”: No such file or directory

第一步:把书中源代码文件夹拷贝到以下目录。



打开此文件夹后,显示如下:



用VS2012打开proj.win32里边的Chapter16_2_CardDefence08.win32.vcxproj,按下图打开:


第二步:在附加包含目录中,添加如下代码:

如果出现fatalerror C1083: 无法打开包括文件:“cocos2d.h”:No such file or directory

解决方法如下:选择项目属性--》C/C++--》附件包含目录设置为:

复制:$(ProjectDir)..\..\..\cocos2dx;$(ProjectDir)..\..\..\cocos2dx\include;$(ProjectDir)..\..\..\cocos2dx\kazmath\include;$(ProjectDir)..\..\..\cocos2dx\platform\win32;$(ProjectDir)..\..\..\cocos2dx\platform\third_party\win32\OGLES;..\Classes;%(AdditionalIncludeDirectories);

同时注意把E:\cocos2d-x-2.2.3\extensions

E:\cocos2d-x-2.2.3\cocosDenshion\android

E:\ cocos2d-x-2.2.3\cocos2dx\platform\third_party\win32\libraries

加到C/C++--》附件包含目录中去。

第三步:把E:\cocos2d-x-2.2.3\Debug.win32下的所有lib文件和dll文件拷贝至自己项目的Debug.win32下面。

最终运行成功画面如下:

cocos2d-iphone – cocos2d convertToWorldSpace

cocos2d-iphone – cocos2d convertToWorldSpace

我不确定convertToWorldspace的工作原理.

>我有spriteA,它是父母.
>我将它设置为40,40位置并将其添加到场景中.
>我有第二个spriteB,我设置在80,80位置.
>我将spriteB添加到spriteA.
>我打印出spriteB的位置:80,80.
>然后我打印[self convertToWorldspace:spriteB.position].而且,我仍然得到80,80.
spriteB的位置应该不同吗?

在你的情况下,如果你想知道你的spriteB在当前世界中的位置,你必须从其父级(spriteA)调用“convertToWorldspace”方法:

[spriteA convertToWorldspace:spriteB.position];

“convertToWorldspace:”方法将节点的变换应用于给定位置.你应该总是从父亲的精灵中调用这个方法.

cocos2d-iphone – Cocos2d只显示CCSprite的一部分

cocos2d-iphone – Cocos2d只显示CCSprite的一部分

是否有可能仅显示CCSprite的一部分?

它接缝的contentSize属性没有很好的结果.

解决方法

doc_180和James的答案都是通过使用纹理的一部分创建新的CCSprite来实现的,但是如果你使用剪切方法,你将获得使用完整纹理但只能在屏幕上绘制其一部分的CCSprite.此方法的一个优点是,您可以修改要在运行中显示或隐藏的部分的大小,而不必一次又一次地重新创建CCSprite(或一次又一次地替换纹理).

因此,要使用剪切方法,只需下载ClippingNode class from here,并将要剪切的CCSprite添加到该ClippingNode.然后调用其中一个方法来指定限制绘图的区域.我目前正在使用它来创建一个进度条,所以我肯定它确实很有用.

关于Cocos2D教程:使用SpriteBuilder和Cocos2D 3.x开发横版动作游戏——Part 2cocos creator横屏的问题就给大家分享到这里,感谢你花时间阅读本站内容,更多关于cocos2d html5 3.8.1 .修复Cocos2d Particle Builder plist 粒子文件 base64 图片 解压错误、Cocos2d 卡牌塔防 游戏 cocos2d-x游戏开发之旅 第14 15 16 章 源代码调试 注意点 出现无法打开包括文件:“cocos2d.h”: No such file or direct、cocos2d-iphone – cocos2d convertToWorldSpace、cocos2d-iphone – Cocos2d只显示CCSprite的一部分等相关知识的信息别忘了在本站进行查找喔。

本文标签: