首先来个背景简介吧,除了游戏高玩,可能很多开发者并不知道Scott Pilgrim这款游戏。实际上它是改编自经典漫画Scott Pilgrim vs. the World,由Ubisoft开发,最初发行在Xbox live上。该系列漫画的详细信息请参考维基百科:http://en.wikipedia.org/wiki/Scott_Pilgrim
好吧,如果你还是对这类游戏毫无头绪,想想曾经玩过的三国无双,还有,。。。 DNF,是的,就DNF这种类似街机的格斗过关游戏。如果连DNF也不知道,那你来看这篇教程干毛?
简单而言,横版格斗过关游戏的真谛就是,一个打不死的英雄小强靠着一双拳头或其它武器把碰到的所有敌人打个稀巴烂。美其名曰:横版格斗过关游戏。
在这篇教程中,我们将学习如何制作一款简约不简单的横版格斗过关游戏。在这个过程中,我们将要学习如何保存动作状态,hitbox(碰撞盒,用于判断物体碰撞),添加方向键,以及添加简单的敌方AI,还有更多其它的。
在开始阅读正式内容之前,你得对cocos2d有所了解。如果从来没接触过cocos2d,那么不妨看看这两个系列的教程。http://blog.sina.com.cn/s/blog_4b55f6860100s9y7.html
和http://blog.sina.com.cn/s/blog_4b55f6860100s9g6.html
开发环境(Xcode4.5.2+Mountain Lion+iOS6.0.1+cocos2d v2.1 beta2)
准备好了吗?来吧,让我们开始大杀四方。
开始前的准备:在Cocos2D中启用ARC
从iOS5开始,苹果引入了ARC的概念,也就是自动引用计数。使用这一计数,iOS开发人员不用再为手动保持和释放内存而头大。只要启用了ARC,编译器会帮我们自动完成内存保持和释放的工作。这样一来,开发者对内存泄露可能导致的问题就放心多了。
不过遗憾的是,标准Cocos2D模板并不能创建自动启用ARC的项目,为此,我们需要对标准Cocos2D项目稍作修改。
从网上下载最新的Cocos2D 2.x,解压缩,并通过以下明了安装模板。
- ./install-templates.sh –f –u
启动Xcode,选择iOS\Cocos2D v2.x\Cocos2D iOS模板创建一个新项目,并将其命名为PompaDroid。暂时别管Company Identifier神马的,但记住把Device Family选作iPhone。点击Next,把项目保存在自己习惯的地方。
为神马把项目取名为PompaDroid?好吧,这个游戏中主角的发型是时髦的Pompadour(其实就是当年风靡一时的飞机头)。然后这哥们打算摧毁他遇到的所有人形机器人(是的,droid这个词很熟,还记得Android这个名字的由来吗?如果不知道,建议去google和维基一下)对android开发者,本人表示歉意,绝不存在任何平台歧视,谁让Andy Rubin是个机器人高玩呢。
在打完电话骚扰Andy Rubin后,我们可以开始给这个cocos2d项目启用ARC了。如果要把整个cocos2d框架修改为支持ARC,实在是费时费力。因此,我们只需要通知编译器哪些元素不支持ARC(比如Cocos2D库中的代码),哪些元素支持ARC(我们自己编写的代码),就ok了。
在Project Navigator(左侧边栏)中选择项目,然后选择TARGETS中的PompaDroid。然后选择Build Phases选项卡,点开Compile Sources 的下三角。现在我们需要告诉编译器,哪些Objective-C类不支持ARC。方法是在Compiler Flags这一列中输入-fno-objc-arc。
具体操作如下:选择所有.m文件然后回车,在弹出窗口中输入-fno-objc-arc,然后再次回车。继续操作,直到除了IntroLayer.m外的所有.m文件都有该标记,最终结果如图所示:
接下来,切换到Build Setting选项卡,查找Objective-C Automatic Reference Counting,然后将其设置为YES。接下来,在Summary选项卡中,将Deployment Target设置为5.0或更高版本。
编译运行项目,看看一切是否正常。如果不出意外,应该会看到如下图的界面。
恭喜贺喜道喜,现在第一步已经完工,这个Cocos2d项目已经启用对ARC的支持了。
游戏场景
好吧,既然基本的hello world模板已经顺利运作,现在就可以来创建游戏场景了。游戏场景(或者主场景,动作场景,随便怎么叫)主要用来显示游戏中所发生的事情,以及游戏机制的处理。在Ray之前的教程中,通常将其命名为ActionLayer
使用Command +N 创建一个新文件,选择iOS\Cocos2D v2.x\CCNode Class模板。将其设置为CCScene的子类,并命名为GameScene。
用类似的方法创建两个新类,不过要设置为CCLayer的子类。将其中一个命名为GameLayer,另一个命名为HudLayer.
最后,删除原有的HelloWorldLayer.h和HelloWorldLayer.m文件,因为后面不会再用到了。记得删除的时候要选择Move to Trash。
此时你的项目结果将如下图所示:
如果此时编译运行项目,会提示IntroLayer.m无法找到HelloWorldLayer.h。因此在IntroLayer.m中对代码做出以下调整。
- //删除这行代码:
- #import "HelloWorldLayer.h"
- //添加这行代码:
- #import "GameScene.h"
- //修改onEnter方法:
- -(void) onEnter
- {
- [superonEnter];
- [[CCDirectorsharedDirector] replaceScene:[CCTransitionFadetransitionWithDuration:1.0scene:[GameScenenode] withColor:ccWHITE]];
- }
这个很好理解。在游戏启动后,AppDelegate需要使用IntroLayer.m来进入游戏,并切换道下一个场景。由于HelloWorldLayer已经被删除,当然需要用GameScene来替代其为止。
接下来切换道GameScene.h,并修改其中的代码:
在文件顶部添加:
- #import "GameLayer.h"
- #import "HudLayer.h"
然后在@end之前添加以下代码:
- @property(nonatomic,weak)GameLayer *gameLayer;
- @property(nonatomic,weak)HudLayer *hudLayer;
然后切换到GameScene.m,并在@implementation中添加init方法:
- -(id)init{
- if((self = [superinit])){
- _gameLayer = [GameLayernode];
- [selfaddChild:_gameLayerz:0];
- _hudLayer = [HudLayernode];
- [selfaddChild:_hudLayerz:1];
- }
- returnself;
- }
init方法中的代码很简单。首先我们创建了GameLayer和HudLayer的实例变量,并将它们添加为GameScene的子节点。Z值代表在屏幕中显示的前后顺序,z值越低越会首先绘制,而z值高的则绘制在其上。
在这个游戏中,GameLayer中将会包含游戏中的主要角色,场景等,而HudLayer则用来显示辅助视觉元素,比如方向键。HudLayer需要绘制在GameLayer之上,因为它负责操控。
注意:对于有过Objective-C编程经验的大牛们来说,以上代码看上去有点怪怪的。你看吧,我们根本没有声明过实例变量,也从来没有合成过属性,怎么就能直接使用呢?
好吧,在最新版本的Xcode中,属性是自动合成的,同时它们也有自己的实例变量,也就是在属性名称前添加下划线即可。这些根本不需你多写一行代码。如果你还是有点困惑,不妨查看iOS 6 by Tutorials(http://www.raywenderlich.com/store/ios-6-by-tutorials)系列教程中的”Modern Objective-C Syntax”.
不过凡事有利有弊,使用这种方式自动创建的实例变量是该类所私有的,甚至其子类都无法直接访问这种实例变量。
所以说,只要你用的是Xcode4.5.1或者以上版本的Xcode,那么就可以放心大胆的这样来写代码。
废话结束。
编译运行,你会看到。。。
一片漆黑,漆黑一片,不给银子,你还能期待神马?
加载瓦片地图
漆黑的屏幕并不能带你走向光明,我们需要在场景中添加点实际的东西。既然是横版过关游戏,那么处在一切场景内容之下的当然是-地图。这里我假定你已经很清楚如何创建瓦片地图。如果不懂,回过头看看这篇教程吧。
http://blog.sina.com.cn/s/blog_4b55f6860100s9g6.html
准备好了吗?首先下载提前制作的资源素材(http://cdn4.raywenderlich.com/downloads/pd_resources.zip),感谢Hunter Russell的努力。
下载后解压缩,然后把Sprites文件夹中的内容拖到项目的Resources中,记得选中Copy items into destination group’s folder,选中Create groups for any added folders,选中Add to targets的PompaDroid,然后点击Finish。
Sprites文件夹里面包含了几个文件,不过这里主要用到了其中的两个:
pd_titles.png
pd_tilemap.tmx 要加载到cocos2d中的真实瓦片地图。
使用Tiled打开pd_tilemap.tmx,就可以看到游戏的整个地图。
这里有几点很重要的事情要说明下:
1.
2.
3.
了解了地图之后,让我们回到代码中来。切换到GameLayer.h,并在@interface后添加以下变量:
CCTMXTiledMap *_tileMap;
然后切换到GameLayer.m,并在@implementation后添加两个方法:
- -(id)init{
- if((self = [superinit])){
- [selfinitTileMap];
- }
- returnself;
- }
- -(void)initTileMap{
- _tileMap = [CCTMXTiledMaptiledMapWithTMXFile:@"pd_tilemap.tmx"];
- for(CCTMXLayer *child in [_tileMapchildren]){
- [[child texture]setAliasTexParameters];
- }
- [selfaddChild:_tileMapz:-6];
- }
initTileMap方法中首先根据所提供的.tmx文件创建了一个瓦片地图。然后遍历瓦片地图的所有子成员,并调用setAliasTexParameters。该方法的作用是对瓦片地图中的所有纹理元素关闭抗锯齿效果,这样就能保证纹理的像素风格,即便地图缩放的时候也是如此。关于像素风格,可以参考这篇教程(http://www.raywenderlich.com/14865/introduction-to-pixel-art-for-games)。
当然,最后我们需要把瓦片地图添加到场景的层中。记住瓦片地图的z值是-6。如果你想让它位于其它一切视觉元素的底部,就得确保所有其它视觉元素的z值必须大于-6。
在编译运行之前,切换到AppDelegate.m,并找到以下代码:
if( ! [director_enableRetinaDisplay:YES] )
将其替换为:
if( ! [director_enableRetinaDisplay:NO] )
之所以要多此一举,是因为这里没有提供针对Retina分辨率的图像。并不是说这款游戏就不支持Retina设备了,而是说当在Retina设备上运行的时候,所有的图像会自动缩放以适应更大的分辨率。如果不这样设置的话,单瓦片地图就只会占据Retina设备的一半屏幕。
编译运行项目,会看到以下的画面:
创建“飞机头”英雄
在大多数2d横版游戏中,各类角色都有代表不同动作的动画。这样问题就来了,我们怎么知道什么时候播放什么动画呢?
在这款横版格斗过关游戏中,我们将使用state machine(状态机)来解决这一问题。
不要被这个名词吓倒,在任何时候碰到这种看似很高深的名词时,请记住银河系漫游指南中的终极提示:不要恐慌,Don’t Panic。所谓的状态机,其实就是某种通过状态切换而更改行为的东西。单个状态机在同一时点只能处于一种状态,但可以从一种状态切换到另一种状态。
为了更好的理解状态机,我们以游戏的基本角色为例,列出他可以做的事情:
1.
2.
3.
根据这个可以列出每种状态下他可以做的事情
1.
2.
3.
然后将这个列表扩展到更多行为:受伤,死亡,那么就总共有五种状态:
基于上面的信息,我们可以说这个角色,如果他是一个状态机,就可以在空闲状态,行走状态,出拳状态,受伤状态和死亡状态间来回切换。
为了在状态间完成切换,每种状态需要有一个必要条件和一个结果。比如,行走状态不能突然切换到死亡状态。因为你的英雄在达到死亡状态前会先受伤(除非被直接爆头?在这款游戏中没有这种设定)。
每种状态的结果都可以解决此前的问题。也就是说,当角色切换到另一种状态时,角色的动画也会随之切换。
好了,关于状态机就先说这么多吧。
如果你要了解更多,请维基(http://en.wikipedia.org/wiki/State_machine)。
注意:为了简便起见,由于人物的每种动作都代表一个状态,后面可能会交叉使用动作和状态。
使用Command-N创建一个新文件,选择iOS\Cocos2D v2.x\CCNode Class模板。将其选择为CCSprite的子类,并命名为ActionSprite.
切换到ActionSprite.h,并在@end前添加以下代码:
- //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 actionState;
- //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;
以上代码声明了用于创建ActionSprite的基本变量和方法,可以分为以下几类:
Actions:这些是每种状态下要执行的CCActions(Cocos2d动作)。CCActions动作是当角色切换状态时的精灵动画和其它触发事件。
States:保存精灵的当前动作/状态,这里使用ActionState来定义,很快我们将对其进行定义。
Attributes:一些角色属性值,包括精灵的行走速度,受伤时的失血值,以及攻击值。
Movement:稍后用于计算精灵如何沿着地图运动
Measurements:其中保存的数值和精灵图片的显示有关。之所以要用到这些数值,是因为所使用精灵图片的画布大小要比其中的图像大很多。
Action methods:在程序中我们不会直接调用CCActions,而是使用这些方法来触发每种状态。
Scheduled methods:以一定时间间隔进行更新的任何其它事情,比如精灵位置和速度的更新,等等。
在切换到ActionSprite.m实现这些方法前,我们需要定义ActionState类型。
为统一和方便起见,本人习惯把所有的定义保存在一个文件中。
点击Command-N创建一个新的文件,选择iOS\C and C++\Header File模板,然后将其命名为Defines。
打开Defines.h,并在#define PompaDroid_Defines_h后面添加以下代码:
- //1 -convenience measurements
- #define SCREEN [[CCDirector sharedDirector]winSize]
- #define CENTER ccp(SCREEN.width/2,SCREEN.height/2)
- #define CURTIME CACurrentMediaTime()
- //2 -convenience functions
- #define random_range(low,high) (arc4random()%(high-low+1))+low
- #define frandom (float)arc4random()/UINT64_C(0x100000000)
- #define frandom_range(low,high) ((high-low)*frandom)+low
- //3 -enumerations
- typedefenum _ActionState{
- kActionStateNone = 0,
- kActionStateIdle,
- kActionStateAttack,
- kActionStateWalk,
- kActionStateHurt,
- kActionStateKnockedOut,
- }ActionState;
- //4 -structures
- typedefstruct _BoundingBox{
- CGRect actual;
- CGRect original;
- }BoundingBox;
这里还是要简单解释一下:
1.
2.
3.
4.
当然,如果你不嫌麻烦,可以在每个类文件的头部添加一行代码#import “Defines.h”,不过别忘了我们还可以直接在预编译头文件中导入。
在Project Navigator中点开Supporting Files的三角,打开Prefix.pch,然后在#import
之后添加一行代码:
#import “Defines.h”
ok,现在编译运行项目,会看到下面的场景:
神马,你看到的还是和之前一样的空白场景?而且还有几个warning,提示ActionSprite.m没有完全实现?对了,还差点事情没做呢~
切换到GameLayer.h,然后添加以下实例变量:
CCSpriteBatchNode *_actors;
然后切换到GameLayer.m,并在init方法的if循环中添加以下代码:
- [[CCSpriteFrameCachesharedSpriteFrameCache]addSpriteFramesWithFile:@"pd_sprites.plist"];
- _actors = [CCSpriteBatchNodebatchNodeWithFile:@"pd_sprites.pvr.czz"];
- [_actors.texturesetAliasTexParameters];
- [selfaddChild:_actorsz:-5];
以上代码从之前添加到Resources中的资源加载了相应的精灵表单,并创建了一个CCSpriteBatchNode。这个精灵表单将包含后面的所有精灵图片。然后我们将其z值设置为高于CCTMXTiledMap,这样就会显示在地图的上面。
注意:当游戏中需要绘制多个动画精灵时,精灵表单就变得特别游泳。当然你可以选择使用标准的精灵对象,但会大大影响游戏性能,而且场景中的精灵越多就越是如此。
接下来要创建我们的英雄了。还是用Command-n创建一个新文件,选择iOS\Cocos2D v2.x\CCNode Class模板。将其选为ActionSprite的子类,并将其命名为Hero。
在Hero.h的顶部添加一行代码:
#import “ActionSprite.h”
然后切换到Hero.m,并在@implementation:中添加init方法如下:
- -(id)init{
- if((self =[superinitWithSpriteFrameName:@"hero_idle_00.png"])){
- int i;
- //idle animation
- CCArray *idleFrames = [CCArrayarrayWithCapacity:6];
- for(i=0;i<6;i++){
- CCSpriteFrame *frame = [[CCSpriteFrameCachesharedSpriteFrameCache]spriteFrameByName:[NSStringstringWithFormat:@"hero_idle_d.png",i]];
- [idleFrames addObject:frame];
- }
- CCAnimation *idleAnimation = [CCAnimationanimationWithSpriteFrames:[idleFrames getNSArray] delay:1.0/12.0];
- self.idleAction = [CCRepeatForeveractionWithAction:[CCAnimateactionWithAnimation:idleAnimation]];
- self.centerToBottom = 39.0;
- self.centerToSides = 29.0;
- self.hitPoints = 100.0;
- self.damage = 20.0;
- self.walkSpeed = 80;
- }
- returnself;
- }
在以上方法中,我们首先使用初始空闲状态下的精灵帧创建了英雄角色,然后创建了一个CCArray对象包含所有属于空闲动画状态下的精灵帧,并创建了一个CCAction动作来播放这个动画。
这个动画很简单,只是以特定间隔逐帧显示空闲状态的英雄精灵帧图片,这里是1.0/12.0,也就是每秒12帧。
接着,我们为英雄设置了一些初始属性,包括精灵的位置。为了更好的理解,这里还是用个图来显示:
英雄的每个帧都在280*150大小的画布上创建,但实际上英雄本身只占据这个空间的一部分。每个CCSprite精灵都有一个属性叫contentSize,也就是帧的大小。对于精灵占据了整个帧的图片该属性比较有用,但这里就不太适用了。因此为了更好的在屏幕中显示英雄,有必要适用这两个测量变量作为辅助。
你可能会想为什么精灵要占据所有的额外空白空间,那是因为每个动画中精灵的绘制方式是不同的,而有些就需要占据更多的空间。通过这种方式,我们为绘制每个帧预留了空间,而不是为动画中的不同精灵设置不同的大小。
接下来切换到GameLayer.h,并添加以下代码:
在文件头部添加:
#import “Hero.h”
然后在@interface部分添加:
Hero *_hero;
接下来切换到GameLayer.m,并添加以下代码:
//在init方法的if循环中,在[self addChild:_actors z:-5]的后面添加代码:
[self initHero];
然后在@implementation部分添加一个新方法如下:
- -(void)initHero{
- _hero = [Heronode];
- [_actorsaddChild:_hero];
- _hero.position = ccp(_hero.centerToSides,80);
- _hero.desiredPosition = _hero.position;
- [_heroidle];
- }
以上方法中我们创建了Hero的实例变量,将其添加到精灵表单中,并设置了其位置。然后调用idle方法让其处于空闲状态,并运行相关动画。这里我们看到了对_centerToSides属性的使用,从而将英雄放置在屏幕的边缘,并完全可见。
不过我们还没有实现idle方法。切换到ActionSprite.m,并在@implementation部分实现该方法:
- -(void)idle{
- if(_actionState != kActionStateIdle){
- [selfstopAllActions];
- [selfrunAction:_idleAction];
- _actionState = kActionStateIdle;
- _velocity = CGPointZero;
- }
- }
仅当ActionSprite不处在空闲状态的时候才会调用idle方法。当触发该方法时,会运行idle动作,并将状态切换为kActionStateIdle,并将其速度修正为0.
编译运行游戏,会看到英雄现在百无聊赖。
挥拳动作
当然,如果我们的英雄只会站在那里发呆,就根本谈不上格斗过关了。接下来我们要教会他挥舞双拳。
和刚才的发呆状态类似,我们需要准备相关的精灵帧和动作。切换到Hero.m,并添加以下代码:
- //attack animation
- CCArray *attackFrames = [CCArrayarrayWithCapacity:3];
- for (i = 0; i <3; i++)
- {
- CCSpriteFrame *frame = [[CCSpriteFrameCachesharedSpriteFrameCache] spriteFrameByName:[NSStringstringWithFormat:@"hero_attack_00_d.png", i]];
- [attackFrames addObject:frame];
- }
- CCAnimation *attackAnimation = [CCAnimationanimationWithSpriteFrames:[attackFrames getNSArray] delay:1.0/24.0];
- self.attackAction = [CCSequenceactions:[CCAnimateactionWithAnimation:attackAnimation], [CCCallFuncactionWithTarget:selfselector:@selector(idle)], nil];
当然,和之前的代码有所差别。首先,攻击动作每秒24帧,其次,攻击动作只会调用攻击动画一次,然后就通过调用idle方法恢复到空闲状态。
切换到ActionSprite.m,并添加以下方法:
- -(void)attack{
- if(_actionState == kActionStateIdle ||_actionState == kActionStateAttack ||_actionState == kActionStateWalk){
- [selfstopAllActions];
- [selfrunAction:_attackAction];
- _actionState = kActionStateAttack;
- }
- }
这里会看到攻击动作的几个限制。只有在英雄之前处于空闲,攻击或行走状态时才能切换到攻击动作。这样确保英雄在被k,或者被Ko的时候无法进行攻击。
为了触发攻击动作,切换到GameLayer.m,并添加以下代码:
在init中添加代码:
self.isTouchEnabled = YES;
然后添加以下方法:
- -(void)ccTouchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
- [_heroattack];
- }
通过以上方法,只要玩家触碰屏幕,就可以发起攻击动作。
编译运行游戏,尝试着挥舞下拳头。
创建8向的方向键
接下来要做的事情就是让英雄可以在地图上移动。很多横版格斗游戏之前是针对掌机设计的,而大多数掌机都使用方向键来控制玩家角色。因此,在这个游戏里,我们将制作虚拟的8向方向键来移动英雄。
首先来创建D-pad类。点击command-n创建一个新的文件,选择iOS\Cocos2D v2.x\CCNode Class模板。将其设置为CCSprite的子类,并命名为SimpleDPad。
打开SimpleDPad.h,并修改其中的代码如下:
- #import
- #import "cocos2d.h"
- @classSimpleDPad;
- @protocol SimpleDPadDelegate
- -(void)simpleDPad:(SimpleDPad *)simpleDPad didChangeDirectionTo:(CGPoint)direction;
- -(void)simpleDPad:(SimpleDPad *)simpleDPad isHoldingDirection:(CGPoint)direction;
- -(void)simpleDPadTouchEnded:(SimpleDPad *)simpleDPad;
- @end
- @interface SimpleDPad : CCSprite{
- float _radius;
- CGPoint _direction;
- }
- @property(nonatomic,weak) id delegate;
- @property(nonatomic,assign) BOOL isHeld;
- +(id)dPadWithFile:(NSString*) fileName radius:(float)radius;
- -(id)initWithFile:(NSString *)filename radius:(float)radius;
- @end
其中有很多内容,这里有几点重要的需要解释:
radius其实就是D-pad方向键的半径。
Direction是当前所按压的方向。它是一个矢量,(-1.0,-1.0)代表左下,(1.0,1.0)代表右上。
Delegate 是D-pad的代理,后续将详细解释。
isHeld是个布尔变量。只要玩家还在触碰D-pad方向键,该变量的值就会是YES。
对SimpleDPad这个类来说,其中用到了一个重要的设计模式-代理模式。也就是说一个代理类(并非SimpleDPad)将会处理由被代理类(SimpleDPad)所启动的任务。
通过这种方式,可以让SimpleDPad忽略任何游戏逻辑,这样就可以在任何其它游戏中使用它。
下图将有助于理解D-pad的代理作用。
当SimpleDPad检测到D-pad方向键中的触碰事件时,就会计算触碰的方向,并向其代理类发送消息告知方向。而接下来的事情就不是SimpleDPad类需要考虑的了。
为了强化代理模式,SimpleDPad需要了解其代理的相关信息,特别是将把触碰方向传给代理类的方法。这里就用到了另一个设计模式:协议。
让我们回过头来看上面的代码,然后看看@protocol中的内容。这里定义了SimpleDPad的代理所需实现的方法。通过这种方式,SimpleDPad强制要求其代理实现这三个方法。这样就可以确保在将相关信息差undigei代理时可以调用其中的任一方法。
实际上,SimpleDPad本身也需要遵循一个协议,也就是
以上协议用于让启用了触摸事件支持的类优先处理这些触摸事件,而避免让其它类在此时接收触摸事件。当SimpleDPad被触碰时,就会优先处理这些触摸事件,而GameLayer此时将无法获得这些触摸事件。别忘了,如果GameLayer被触碰,那么英雄就会大开杀戒,而显然这不是你在触摸D-pad方向键时希望看到的。
现在切换到SimpleDPad.m,并更改其中的代码如下:
- //
- // SimpleDPad.m
- // PompaDroid
- //
- // Created by Allen Benson G Tan on 10/21/12.
- // Copyright 2012 __MyCompanyName__. All rights reserved.
- //
- #import "SimpleDPad.h"
- @implementation SimpleDPad
- +(id)dPadWithFile:(NSString *)fileName radius:(float)radius
- {
- return [[selfalloc] initWithFile:fileName radius:radius];
- }
- -(id)initWithFile:(NSString *)filename radius:(float)radius
- {
- if ((self = [superinitWithFile:filename]))
- {
- _radius = radius;
- _direction = CGPointZero;
- _isHeld = NO;
- [selfscheduleUpdate];
- }
- returnself;
- }
- -(void)onEnterTransitionDidFinish
- {
- [[[CCDirectorsharedDirector] touchDispatcher] addTargetedDelegate:selfpriority:1swallowsTouches:YES];
- }
- -(void) onExit
- {
- [[[CCDirectorsharedDirector] touchDispatcher] removeDelegate:self];
- }
- -(void)update:(ccTime)dt
- {
- if (_isHeld)
- {
- [_delegatesimpleDPad:selfisHoldingDirection:_direction];
- }
- }
- -(BOOL)ccTouchBegan:(UITouch *)touch withEvent:(UIEvent *)event
- {
- CGPoint location = [[CCDirectorsharedDirector] convertToGL:[touch locationInView:[touch view]]];
- float distanceSQ = ccpDistanceSQ(location, position_);
- if (distanceSQ <= _radius * _radius)
- {
- //get angle 8 directions
- [selfupdateDirectionForTouchLocation:location];
- _isHeld = YES;
- returnYES;
- }
- returnNO;
- }
- -(void)ccTouchMoved:(UITouch *)touch withEvent:(UIEvent *)event
- {
- CGPoint location = [[CCDirectorsharedDirector] convertToGL:[touch locationInView:[touch view]]];
- [selfupdateDirectionForTouchLocation:location];
- }
- -(void)ccTouchEnded:(UITouch *)touch withEvent:(UIEvent *)event
- {
- _direction = CGPointZero;
- _isHeld = NO;
- [_delegatesimpleDPadTouchEnded:self];
- }
- -(void)updateDirectionForTouchLocation:(CGPoint)location
- {
- float radians = ccpToAngle(ccpSub(location, position_));
- float degrees = -1 * CC_RADIANS_TO_DEGREES(radians);
- if (degrees <= 22.5&& degrees >= -22.5)
- {
- //right
- _direction = ccp(1.0, 0.0);
- }
- elseif (degrees >22.5&& degrees <67.5)
- {
- //bottomright
- _direction = ccp(1.0, -1.0);
- }
- elseif (degrees >= 67.5&& degrees <= 112.5)
- {
- //bottom
- _direction = ccp(0.0, -1.0);
- }
- elseif (degrees >112.5&& degrees <157.5)
- {
- //bottomleft
- _direction = ccp(-1.0, -1.0);
- }
- elseif (degrees >= 157.5 || degrees <= -157.5)
- {
- //left
- _direction = ccp(-1.0, 0.0);
- }
- elseif (degrees < -22.5&& degrees > -67.5)
- {
- //topright
- _direction = ccp(1.0, 1.0);
- }
- elseif (degrees <= -67.5&& degrees >= -112.5)
- {
- //top
- _direction = ccp(0.0, 1.0);
- }
- elseif (degrees < -112.5&& degrees > -157.5)
- {
- //topleft
- _direction = ccp(-1.0, 1.0);
- }
- [_delegatesimpleDPad:selfdidChangeDirectionTo:_direction];
- }
- @end
在以上方法中,首先是两个初始化方法。接下来的onEnterTransitionDidFini
最后的几个方法其实也比较简单:
ccTouchBegan:检查触摸位置是否在方向键的圆形区域内。如果是,就会将isHeld设置为YES,并更新方向值。该方法同时会返回YES以拥有对触摸事件的处理优先权。
ccTouchMoved:每当触摸点移动时,更新方向值。
ccTouchEnded:当触摸事件结束时,关闭isHeld属性,将方向重置,并通知代理触摸事件已经结束。
updateDirectionForTouchL
好了,D-pad类大功告成。现在我们将会把它添加到游戏中。既然是方向控制区,当然要放在所有视觉元素的顶部了。所以我们将会在HudLayer层中添加它。
切换到HudLayer.h,并更改其中的代码如下:
- #import
- #import "cocos2d.h"
- #import "SimpleDPad.h"
- @interface HudLayer : CCLayer {
- }
- @property(nonatomic,weak)SimpleDPad *dPad;
- @end
然后切换到HudLayer.m,并添加以下方法:
- -(id)init
- {
- if ((self = [superinit]))
- {
- _dPad = [SimpleDPaddPadWithFile:@"pd_dpad.png"radius:64];
- _dPad.position = ccp(64.0, 64.0);
- _dPad.opacity = 100;
- [selfaddChild:_dPad];
- }
- returnself;
- }
以上方法中创建并初始化了一个SimpleDPad实例变量,将其添加为HudLayer的子节点。此时,GameScene场景会同时处理GameLayer和HudLayer这两个层。不过有时你可能希望从HudLayer直接访问GameLayer.
切换到GameLayer.h,并添加以下代码:
#import “SimpleDPad.h”
#import “HudLayer.h”
然后更改类声明为:
@interface GameLayer : CCLayer
接下来声明一个新的属性:
@property(nonatomic,weak)HudLayer *hud;
这样我们就在GameLayer中添加了到HudLayer的弱引用。同时还让GameLayer遵循SimpleDPad类所创建的协议。
好了,现在切换到GameScene.m,然后在init方法中添加以下代码:
- _hudLayer.dPad.delegate = _gameLayer;
- _gameLayer.hud = _hudLayer;
编译运行游戏,就会看到左下出现了虚拟的方向键区。
不过预先警告一下,现在别随便碰按键区,因为我们还没有实现协议方法。如果这时候触碰按键区,游戏会崩溃。
剩下的内容我们会在教程的第二部分来实现,稍安勿躁。
到目前为止的项目源代码: