Cocos2D教程:使用SpriteBuilder和Cocos2D 3.x开发横版动作游戏
本文是对教程How To Make A Side-Scrolling Beat Em Up Game Like Scott Pilgrim with Cocos2D – Part 1的部分翻译,加上个人理解而成,最重要的是将文中所有代码转换为Cocos2D 3.x版本。众所周知,3.x与2.x的区别非常之大,在触摸机制、渲染机制等方面都与之前版本有了本质的区别。这里将本人摸索的结果加上,供大家参考。 通过本系列教程你可以学到: 1、Cocos2D 3.x版本的工程创建以及编写 2、TiledMap瓦片地图的简单使用 3、角色状态机的使用 4、敌人AI与简单的决策树 5、碰撞与攻击检测 6、虚拟摇杆的封装与使用 ………… 现在,一起来学习吧。 游戏开始:新建游戏工程Cocos2D 3.x的官方推荐新建项目的方法是利用SpriteBuilder,并且再建完项目之后是否继续使用SpriteBuilder就取决于你的游戏了,所以不用担心你对SpriteBuilder一无所知,因为该游戏中我们不会用到SpriteBuilder。打开SpriteBuilder,点击左上角File->new->Project
给项目起一个名字:PompaDroid,按照作者的意思,就是海扁机器人(Android)喽,这里语言选择Objective-C 工程建好之后,点击左上角发布按钮
然后点击file->open project in Xcode,这时SpriteBuilder的任务就完成了,在打开的Xcode中编译,运行,你就可以看到上面的SpriteBuilder的画面了,这就是3.x版本的HelloWorld界面。
游戏主场景该游戏中只需要一个场景,因为我们去掉了所谓的开始界面、结束界面等,也没有加入什么装备界面或者是任务界面。将原本项目中自带的MainScene删掉,我们不需要这个类。现在,我们先把框架搭好。按下command+N,新建一些Objective-C类,分别是: GameScene——我们的游戏主场景,主要功能是将实现游戏功能的两个Layer添加进来。 GameLayer——核心类之一,处理触摸(攻击),加载瓦片地图,实现游戏逻辑。 HUDLayer——放置虚拟摇杆的Layer,与GameLayer分开的原因后面会讲到。
建好以后,你的工程应该会类似这样:
当然了,如果你现在想编译运行,你还会发现你的项目在一开始就crash了,因为我们刚才已经把项目的入口删掉了,现在我们要换成我们自己的入口。 使用SpriteBuilder创建的项目与之前版本有很大的不同,尤其是在AppDelegate中,自习阅读一下的话,会发现这里干的事情是加载SpriteBuilder中的一些配置。我们之前熟悉的代码被提交到CCAppDelegate中了。 打开Soucre->Platforms->iOS->AppDelegate.m,找到最下面的startScene方法,将其替换为
<span style="font-size:18px;">return [GameScene node];</span>不要忘了引入头文件 <span style="font-size:18px;">#import "GameScene.h"</span> 这时编译运行,你就会看到——一片漆黑了。。也对,我们还没有添加任何代码呢。
依然是我们的游戏场景类GameScene中,我们添加如下代码:
<span style="font-size:18px;">//导入头文件 #import "GameLayer.h" #import "HUDLayer.h" //添加属性声明 @property (strong,nonatomic) GameLayer *gameLayer; @property (strong,nonatomic) HUDLayer *hudLayer;</span>在.m中添加初始化方法init <span style="font-size:18px;">- (id)init { self = [super init]; if (self) { self.gameLayer = [GameLayer node]; self.gameLayer.contentSize = CGSizeMake(self.gameLayer.tileMap.tileSize.width * self.gameLayer.tileMap.mapSize.width,self.gameLayer.tileMap.tileSize.height * self.gameLayer.tileMap.mapSize.height); [self addChild:self.gameLayer z:0]; self.hudLayer = [HUDLayer node]; self.hudLayer.contentSize = CGSizeMake(VISIBLE_SIZE.width,VISIBLE_SIZE.height); [self addChild:self.hudLayer z:1]; } return self; }</span>
这里VISIBLE_SIZE是一个方便操作用的宏,后面会给出,这里为了消除报错,简单地设置为[[CCDirector sharedDirector] designSize]即可,实际上这就是该宏的声明。 接下来是时候开始真正的游戏编程了。
加载瓦片地图首先,从这里下载我们工程中会用到的所有的资源。然后将其中Sprite目录添加到我们的工程中,别忘了勾选上Copy items if needed。
如果你打开其中的tiledMap地图,你会发现,每一块瓦片的大小都是32*32,该瓦片地图的从下往上数第三行包含着墙壁和地面两种资源。我们的主角只能在下面三行地面上行走。 回到编码上来,在GameLayer中添加如下属性声明:
@property (strong,nonatomic) CCTiledMap *tileMap;然后在.m中添加方法:
-(id)init { if ((self = [super init])) { [self initTileMap]; } return self; } -(void)initTileMap { self.tileMap = [CCTiledMap tiledMapWithFile:@"pd_tilemap.tmx"]; [self addChild:_tileMap z:-6]; } 3.x中瓦片地图的类是CCTiledMap,这是与之前的一点不同。 现在,编译并运行,你会发先我们的地图已经成功加载进去了。
创建英雄大多数2D游戏中,我们的主角都有各种不同的动画,表示不同的动作。那么现在的问题是:你怎么知道什么时候展示什么动画呢?这就要用到状态机了。一个简单的状态机在同一时刻只有一种状态,每切换一种状态,就切换对应的一个行为,或者说是动画。 本游戏中,我们的主角和敌人都有五种状态: 1、攻击 2、行走 3、受伤 4、死亡 5、平常
另外,状态的切换是有条件的。例如,如果角色正在攻击,那么他不能立刻变成死亡状态(先经过受伤状态)。 理论讲到这里就够了,现在开始编码。 正如之前说的,我们的主角和敌人都有这种状态切换的共性,因此我们抽象成一个超类。按下command+N新建一个类继承自CCSprite,取名为ActionSprite,然后在头文件中添加以下代码:
//actions @property(nonatomic,strong)id idleAction; @property(nonatomic,strong)id attackAction; @property(nonatomic,strong)id walkAction; @property(nonatomic,strong)id hurtAction; @property(nonatomic,strong)id knockedOutAction; //states @property(nonatomic,assign)ActionState state; //attributes @property(nonatomic,assign)float walkSpeed; @property(nonatomic,assign)float hitPoints; @property(nonatomic,assign)float damage; //movement @property(nonatomic,assign)CGPoint velocity; @property(nonatomic,assign)CGPoint desiredPosition; //measurements @property(nonatomic,assign)float centerToSides; @property(nonatomic,assign)float centerToBottom; //action methods -(void)idle; -(void)attack; -(void)hurtWithDamage:(float)damage; -(void)knockout; -(void)walkWithDirection:(CGPoint)direction; //scheduled methods -(void)update:(CCTime)dt;这里分类解释一下: Actions:这五个属性都是CCAction类型的对象,用来执行不同状态下地动作。 States:角色的状态,ActionState是一个枚举类型的变量,稍后给出定义。 Attributes:角色的一些参数。 Measurements:这两个值用于以后角色定位用,因为Cocos2D中精灵的位置是以中心为参照的。 Action Methods:执行动作的方法,这里面包括状态判断与转移。 Scheduled methods:定时器方法,每一帧都会调用。 现在我们来把一些常量定义一下,为了方便,我们将所有常量定义到一个头文件中,新建一个头文件Define.h,然后添加如下代码:
//convenience measurements #define VISIBLE_SIZE [[CCDirector sharedDirector] designSize] #define CENTER ccp(VISIBLE_SIZE.width / 2,VISIBLE_SIZE.height / 2) #define CURRENT_TIME CACurrentMediaTime() //convenience methods #define RANDOM_RANGE(low,high) (low + arc4random() % (high - low + 1)) #define FLOAT_RANDOM ((float)arc4random()/UINT64_C(0x100000000)) #define FLOAT_RANDOM_RANGE(low,high) (low + (high - low) * FLOAT_RANDOM) //action states,enumeration typedef enum ActionState { kActionStateNone = 0,kActionStateIdle,kActionStateAttack,kActionStateWalk,kActionStateHurt,kActionStateKnockedOut } ActionState; //struct typedef struct BoundingBox { CGRect actual; CGRect original; } BoundingBox; 前面的一系列define都是为了方便操作用到,见名也能知意。ActionState就扮演着状态机(严格来说,应该是ActionSprite这个类)的角色。除去那个None状态以外都是之间说过的角色可能会有的状态。最后一项是用于第二篇教程中攻击与被攻击检测中的。此处为了方便,先写上即可。 你可以将该头文件包含在预编译头文件Prefix.pch中,不过从Xcode6将该文件去除来看,Apple已经不支持我们这样了。当然,没什么影响,看个人习惯就好。SpriteBuilder生成的工程带着该文件,那么我们就用它吧。 暂时放着这些方法不管,我们先回到GameLayer,让我们的角色出现在屏幕上再说。 在GameLayer中添加下面的代码: //引入头文件 #import "CCTextureCache.h" //在init方法中添加资源 [[CCSpriteFrameCache sharedSpriteFrameCache] addSpriteFramesWithFile:@"pd_sprites.plist"]; [[CCTextureCache sharedTextureCache] addImage:@"pd_sprites.png"];
接下来,是时候创建我们的主角了。command+N信件一个Hero类继承自ActionSprite。在.m中添加如下代码:
//包含动画类头文件 #import "CCAnimation.h"
//init方法 - (id)init { CCSpriteFrame *frame = [[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName:@"hero_idle_00.png"]; self = [super initWithSpriteFrame:frame]; if (self) { //idle action NSMutableArray *idleFrames = [NSMutableArray arrayWithCapacity:6]; for (int i = 0; i < 6; i++) { CCSpriteFrame *frame = [[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName:[NSString stringWithFormat:@"hero_idle_%02d.png",i]]; [idleFrames addObject:frame]; } CCAnimation *idleAnimation = [CCAnimation animationWithSpriteFrames:idleFrames delay:1.0f / 12.0f]; self.idleAction = [CCActionRepeatForever actionWithAction:[CCActionAnimate actionWithAnimation:idleAnimation]]; self.centerToBottom = 39.0f; self.centerToSides = 29.0f; self.hitPoints = 100; self.walkSpeed = 80; self.damage = 20; } return self; } 这样你就创建好你的主角并且给他idle状态下的动画以及必要的参数值了。这里关于centerToBottom和centerToSides属性详见下图:
回到GameLayer中,包含我们的Hero的头文件并声明一个Hero的属性hero,然后在init方法中调用下面的方法initHero:
- (void)initHero { self.hero = [Hero node]; [self addChild:self.hero z:-5]; self.hero.position = ccp(self.hero.centerToSides,80); self.hero.desiredPosition = self.hero.position; [self.hero idle]; } 接下来去ActionSprite中实现idle方法:
- (void)idle { if (self.state != kActionStateIdle) { [self stopAllActions]; [self runAction:self.idleAction]; self.state = kActionStateIdle; self.velocity = CGPointZero; } }现在编译然后运行,你会在模拟器上发现我们的英雄正在“抖动” :]
攻击!攻击!
接下来在Hero中用同样地方式来创建攻击动作:
//attack action //添加到ilde action之后 NSMutableArray *attackFrames = [NSMutableArray arrayWithCapacity:3]; for (int i = 0; i < 3; i++) { CCSpriteFrame *frame = [[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName:[NSString stringWithFormat:@"hero_attack_00_%02d.png",i]]; [attackFrames addObject:frame]; } CCAnimation *attackAnimation = [CCAnimation animationWithSpriteFrames:attackFrames delay:1.0 / 24.0f]; self.attackAction = [CCActionSequence actionOne:[CCActionAnimate actionWithAnimation:attackAnimation] two:[CCActionCallFunc actionWithTarget:self selector:@selector(idle)]];同样,在ActionSprite中实现attack action - (void)attack { if (self.state == kActionStateAttack || self.state == kActionStateIdle || self.state == kActionStateWalk) { [self stopAllActions]; [self runAction:self.attackAction]; self.state = kActionStateAttack; } } 从这两个动作的实现上大家也应该能够看出所谓的状态机的工作原理了:检查状态——做出动作——切换状态。
下一个问题是,我们什么时候让Hero攻击呢?这里我们规定,当用户触摸屏幕的时候做出攻击,所以这里要用到触摸事件了。
还记得我们之前说过触摸事件触发的三个条件吗?contentSize我们已经有了,接下来在GameLayer的init方法中开启触摸开关:
//添加到init方法中 self.userInteractionEnabled = TRUE;接着重写触摸触发方法: - (void)touchBegan:(CCTouch *)touch withEvent:(CCTouchEvent *)event { [self.hero attack]; } 然后编译运行,触摸屏幕,你会发现你的Hero已经可以攻击了! 虚拟摇杆接下来我们通过创建一个虚拟摇杆来让角色能够移动。虚拟摇杆我就不多废话了,相信大家已经非常熟悉了。这里看一下其实现。
新建一个类SimpleDPad,头文件中添加下述代码:
@class SimpleDPad; @protocol SimpleDPadDelegate <NSObject> - (void)simpleDPad:(SimpleDPad *)dPad didChangeDirectionTo:(CGPoint)direction; - (void)simpleDPad:(SimpleDPad *)dPad isHoldingDirection:(CGPoint)direction; - (void)simpleDPadTouchEnded:(SimpleDPad *)dPad; @end @interface SimpleDPad : CCSprite { CGFloat _radius; CGPoint _direction; //(-1,-1) represent left,bottom while (1,1) represent right,top } @property (assign,nonatomic) BOOL isHeld; @property (weak,nonatomic) id<SimpleDPadDelegate> delegate; +(id)dPadWithFile:(NSString *)fileName radius:(CGFloat)radius; - (id)initWithFile:(NSString *)fileName radius:(CGFloat)radius; @end 可以看出,这里用到的是iOS编程中最常用的设计模式之一——代理模式,其中radius指触摸点与中心水平线的角度,direction表示方向,第一个值-1表示左,1表示右,第二个值-1表示下,1表示上,因此我们的这个严格上来说并不是虚拟摇杆,而只是个8-方向控制器 :[
现在我们来实现一下。
在.m中实现两个初始化方法:
+ (id)dPadWithFile:(NSString *)fileName radius:(CGFloat)radius { return [[self alloc] initWithFile:fileName radius:radius]; } - (id)initWithFile:(NSString *)fileName radius:(CGFloat)radius { self = [super initWithImageNamed:fileName]; if (self) { self.userInteractionEnabled = YES; _radius = radius; _direction = CGPointZero; self.isHeld = NO; } return self; } 然后重写update方法: - (void)update:(CCTime)delta { if (self.isHeld) { [self.delegate simpleDPad:self isHoldingDirection:_direction]; } }
- (void)touchBegan:(CCTouch *)touch withEvent:(CCTouchEvent *)event { CGPoint touchPoint = [[CCDirector sharedDirector] convertToGL:[touch locationInView:touch.view]]; CGFloat distance = ccpDistanceSQ(touchPoint,self.position); if (distance < _radius * _radius) { //get angle 8 directon [self updateDirectionForTouchLocation:touchPoint]; self.isHeld = YES; return ; } [super touchBegan:touch withEvent:event]; } - (void)touchMoved:(CCTouch *)touch withEvent:(CCTouchEvent *)event { CGPoint touchPoint = [[CCDirector sharedDirector] convertToGL:[touch locationInView:touch.view]]; [self updateDirectionForTouchLocation:touchPoint]; } - (void)touchEnded:(CCTouch *)touch withEvent:(CCTouchEvent *)event { _direction = CGPointZero; self.isHeld = NO; [self.delegate simpleDPadTouchEnded:self]; } - (void)updateDirectionForTouchLocation:(CGPoint)location { float radians = ccpToAngle(ccpSub(location,self.position)); CCLOG(@"radians = %f",radians); //to make the angle be positive in clockwise direction float degrees = -1 * CC_RADIANS_TO_DEGREES(radians); CCLOG(@"degrees = %f",degrees); if (degrees <= 22.5 && degrees >= -22.5) { //right _direction = ccp(1.0,0.0); } else if (degrees > 22.5 && degrees < 67.5) { //bottom right _direction = ccp(1.0,-1.0); } else if (degrees >= 67.5 && degrees <= 112.5) { //bottom _direction = ccp(0.0,-1.0); } else if (degrees > 112.5 && degrees < 157.5) { //bottom left _direction = ccp(-1.0,-1.0); } else if (degrees >= 157.5 || degrees <= -157.5) { //left _direction = ccp(-1.0,0.0); } else if (degrees < -22.5 && degrees > -67.5) { //top right _direction = ccp(1.0,1.0); } else if (degrees <= -67.5 && degrees >= -112.5) { //top _direction = ccp(0.0,1.0); } else if (degrees < -112.5 && degrees > -157.5) { //top left _direction = ccp(-1.0,1.0); } if (_direction.x == -1.0) { CCLOG(@"left"); } else if (_direction.x == 1.0) { CCLOG(@"right"); } if (_direction.y == -1.0) { CCLOG(@"bottom"); } else if (_direction.y == 1.0) { CCLOG(@"top"); } [self.delegate simpleDPad:self didChangeDirectionTo:_direction]; } 虽然很长,尤其是那一段if-else块。。但是不难理解。现在一个一个分析:
首先是三个触摸方法,再讲之前我们先来说一个概念——消息链传递。很显然我们不希望当我们按下虚拟摇杆的时候我们的英雄会攻击(别忘了,HUDLayer在GameLayer之后添加),也就是说,我们不希望HUDLayer中得事件传递下去。
我们单独讲一下最后一个方法。
首先我们通过Cocos2D中定义的宏以及数学常识计算出触摸点与中心水平线间的角度(弧度制),将其转换为角度制。这里乘上了一个-1,目的是让角度呈顺时针增加,逆时针减少的趋势。计算出角度之后,分别赋值给八个方向即可。最后,将最终判定出来的方向交给代理去处理就好。
最后,打开我们一直没用的HUDLayer,包含头文件并声明属性:
//包含头文件 #import "SimpleDPad.h" //定义属性 @property (strong,nonatomic) SimpleDPad *dPad;在HUDLayer.m中添加方法: - (id)init { self = [super init]; if (self) { self.userInteractionEnabled = TRUE; self.dPad = [SimpleDPad dPadWithFile:@"pd_dpad.png" radius:64]; self.dPad.position = ccp(64.0,64.0); self.dPad.opacity = 100.0 / 255.0; [self addChild:self.dPad]; } return self; } 然后,切换到GameLayer.h中,包含HUDLayer的头文件,让GameLayer实现协议SimpleDPadDelegate,然后定义一个HUDLayer类型的属性。 //add to top of file #import "SimpleDPad.h" #import "HudLayer.h" //add in between @interface GameLayer : CCNode and the opening curly bracket <SimpleDPadDelegate> //add after the closing curly bracket and the @end @property (strong,nonatomic) HUDLayer *hudLayer; 原文中HUDLayer使用的是弱引用,因为接下来GameLayer持有HUDLayer,HUDLayer强引用SimpleDPad,SimpleDPad通过代理持有GameLayer,但是既然代理已经是weak了,保留环已经被打破,个人感觉这里弱引用强引用皆可,特此实验。
最后一步,如上面所述,在GameScene中,让GameLayer中hudLayer指向之前定义好的hudLayer,同时为hudLayer中dPad的delegate赋值为gameLayer:
//添加到GameScene中init方法里 self.hudLayer.dPad.delegate = self.gameLayer; self.gameLayer.hudLayer = self.hudLayer; 好了,接下来编译运行你的工程吧,你会看到一个虚拟摇杆出现在你的屏幕左下方。 点击你的摇杆,你就能看到Hero移动了!
。。。是不可能的:]
何去何从?
我们还没有实现代理方法,没有为英雄编写walk的动作,没有让英雄真正在地图上动起来,因此你点击虚拟摇杆你的程序crash的时候,不用担心。下一篇教程中你就能看到如何让英雄动起来。同时你还能学到添加机器人、机器人的人工智能与决策树,以及Hero与Robot的互相攻击,Coooooooool~~
到目前为止,你已经通过SpriteBuilder创建了一个Cocos2D 3.x的工程,使用了TiledMap,创建了状态机,实现了idle和attack两个状态与动作,以及封装了一个很cool的虚拟摇杆。
这里有本篇教程的全部源码,欢迎下载。
如果你有任何问题或评论,请在下面留下评论。
接下来我会更新本系列教程的Part2部分,来完善这个游戏。
(编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |