如何设计开发iphone塔防游戏3-炮塔就位
本文由eseedo(泰然骷髅会成员)翻译,泰然授权转载,转载请通知eseedo(http://blog.sina.com.cn/eseedo)。(-by Iven)
欢迎继续我们的塔防游戏学习之旅,今天我们要学习另一块重要的内容-搭建工事,安放炮塔。好吧,我向大家保证,下一章我们会把之前的东西整合在一起。如果之前的两部分还没有看过,最好先去看看吧。
你从上一章中学到了很重要的一点,开发游戏并不需要那么的复杂,只需要让它看上去像那么回事。
对于目前的游戏来说,我们并不需要复杂的A*(http://baike.baidu.com/view/7850.htm), DFS/BFS(传说中令人生畏的深度优先搜索和广度优先搜索算法,如果你是个数学狂人或者算法爱好者,还请移步这里:http://baike.baidu.com/view/288277.htm 和这里:http://baike.baidu.com/view/825760.htm )或者最佳优先搜索算法(http://baike.baidu.com/view/298415.html),一些简单的代码足以让这些敌人在地图上动起来。当然,现在不用并不意味着不能用,只是我们无论是设计还是实现都要尽量遵循K.I.S.S懒人原则(http://baike.baidu.com/view/1649155.htm)。
代码狂人们,觉悟吧,不要一天到晚纠缠于该如何完善哪个算法,你不是科学家,你是攻城师。作为一个攻城师,你的主要任务是满足用户需求,解决用户的问题,而不是成为下一个图灵奖的得主。当然,如果你就是这么计划的,建议你不要和开发游戏的攻城师们混,而要和真正的天才黑客们打拼,否则无论是你的老板,你的客户,还是你自己,都会非常痛苦。
废话少说,先把这一部分的源代码下载下来吧:Cocos2d iPhone Game Tower Defense Tutorial Part 2.
玩家要安放炮塔很简单,只需要从地图上的”Game HUD(游戏信息显示器)“部分将炮塔拖曳到地图的可放置区域即可。每个炮塔都有自己的作用范围,当玩家从game hud中拖曳炮塔放置在地图上时可以看到。好吧,激动人心的时刻到了,如果玩家把炮塔拖曳到一块沙地上,那么我们就在那里放置了一挺机枪。如果玩家把炮塔拖曳到沙地之外的其它地方,那么我们就认为玩家并不想放置武器,只是手抖了点错了而已。至于你信不信,我反正是信了。
你可以发现,只要是有沙地的地方,就可以在那里安放炮塔。我们通过两个方法来实现这一点-“tileCoordForPosition”和”addTower”。”tileCoordForPosition”这个方法可以快速判断当前所在的瓦片,然后使用addTower方法来把炮塔实际放置在地图上(假定那里可以放置)。
好了,让我们打开TutorialScene.m这个文件,来看看这两个方法的具体实现:
- (CGPoint) tileCoordForPosition:(CGPoint) position { int x = position.x / self.tileMap.tileSize.width; int y = ((self.tileMap.mapSize.height * self.tileMap.tileSize.height) - position.y) / self.tileMap.tileSize.height; return ccp(x,y); } -(void)addTower: (CGPoint)pos { DataModel *m = [DataModel getModel]; Tower *target = nil; CGPoint towerLoc = [self tileCoordForPosition: pos]; int tileGid = [self.background tileGIDAt:towerLoc]; NSDictionary *props = [self.tileMap propertiesForGID:tileGid]; NSString *type = [props valueForKey:@"buildable"]; NSLog(@"Buildable: %@", type); if([type isEqualToString: @"1"]) { target = [MachineGunTower tower]; target.position = ccp((towerLoc.x * 32) + 16, self.tileMap.contentSize.height - (towerLoc.y * 32) - 16); [self addChild:target z:1]; target.tag = 1; [m._towers addObject:target]; } else { NSLog(@"Tile Not Buildable"); } }
当用户想要在某处放置炮塔的时候,会调用addTower方法,其参数就是该处的坐标值。然后我们使用tileCoordForPosition获取实际的瓦片位置,再使用该位置获取该瓦片。然后我们来查看该瓦片的属性,判断该处是否属于“buildable(可修建的)”。如果该瓦片的类型是“1”,我们就知道这里可以修建炮塔。然后我们会在该处放置一个新的炮塔,并将其位置设置在瓦片的中心点上。当然,如果瓦片的类型是”0”,就意味着此处不能安放炮塔,我们就心安理得的喝茶去了。
既然上面谈到了炮塔这个类,那么就得看看其中的代码了。
先看看Tower.h这个头文件吧:
#import "cocos2d.h" #import "SimpleAudioEngine.h" #import "DataModel.h" @interface Tower : CCSprite { int _range; CCSprite * selSpriteRange; } @property (nonatomic, assign) int range; @end @interface MachineGunTower : Tower { } + (id)tower; - (void)towerLogic:(ccTime)dt; @end
Tower类继承自CCSprite精灵类,有一个range(范围)属性,它的作用是限制炮塔可以发射炮弹的范围,而selSpriteRange这个精灵则是用图形的方式来视觉化呈现出这个范围。当然,我们还定义了一个MachineGunTower类,它直接继承自Tower类,是对Tower类的扩展。为毛要这么做呢?既然每种炮塔都有自己独一无二的属性,我们应该为之设计独立的分类,从而一目了然。但这些炮塔同时又都具备着一些共性,比如都有攻击范围,都有各自的伤害值。我们将在后续的教程中对Tower基本类和各种炮塔子类进行扩展。
现在来看看Tower.m这个文件的代码:
#import "Tower.h" @implementation Tower @synthesize range = _range; @end @implementation MachineGunTower + (id)tower { MachineGunTower *tower = nil; if ((tower = [[[super alloc] initWithFile:@"MachineGunTurret.png"] autorelease])) { tower.range = 200; [tower schedule:@selector(towerLogic:) interval:0.2]; } return tower; } -(id) init { if ((self=[super init]) ) { //[[CCTouchDispatcher sharedDispatcher] addTargetedDelegate:self priority:0 swallowsTouches:YES]; } return self; } -(void)towerLogic:(ccTime)dt { } @end
上面的代码需要解释的并不多。只是一个基本的类实现。我们载入了一个图像,设置了炮塔的攻击范围,然后创建了一个定时器,从而每隔0.2秒调用一次towerLogic方法。当然,现在这个炮塔只是个花架子,它不需要做任何事情,所以这个方法的内容目前是空的,后续的教程中我们会对此进行补充。
炮塔的事情搞定了,让我们来看看Game Hud(游戏信息显示器)吧。这个hud里面包含了所有可选的炮塔,你可以从中挑选一种炮塔将其拖曳到游戏层。从现在起,我们在屏幕上将拥有两个不同的层。
让我们看看GameHud的头文件:
#import "cocos2d.h" @interface GameHUD : CCLayer { CCSprite * background; CCSprite * selSpriteRange; CCSprite * selSprite; NSMutableArray * movableSprites; } + (GameHUD *)sharedHUD; @end
来看看上面的代码吧:background就是放置到CCLayer的背景图片,selSprite是炮塔图片的复制,当玩家从游戏层选择这个炮塔的时候,我们会在屏幕上拖动它。selSpriteRange就是代表攻击范围的图片。然后我们还定义了一个moveableSprites数组,里面存放了我们将要在游戏层上显示的炮塔精灵。
接下来看看GameHUD实现文件中的init方法:
-(id) init { if ((self=[super init]) ) { CGSize winSize = [CCDirector sharedDirector].winSize; //画出游戏HUD的背景 [CCTexture2D setDefaultAlphaPixelFormat:kCCTexture2DPixelFormat_RGB565]; background = [CCSprite spriteWithFile:@"hud.png"]; background.anchorPoint = ccp(0,0); [self addChild:background]; [CCTexture2D setDefaultAlphaPixelFormat:kCCTexture2DPixelFormat_Default]; //加载炮塔图片,并将它们绘制到游戏HUD层,你当然可以用四个完全不同的图片来代表不同的炮塔 movableSprites = [[NSMutableArray alloc] init]; NSArray *images = [NSArray arrayWithObjects:@"MachineGunTurret.png", @"MachineGunTurret.png", @"MachineGunTurret.png", @"MachineGunTurret.png", nil]; for(int i = 0; i < images.count; ++i) { NSString *image = [images objectAtIndex:i]; CCSprite *sprite = [CCSprite spriteWithFile:image]; float offsetFraction = ((float)(i+1))/(images.count+1); sprite.position = ccp(winSize.width*offsetFraction, 35); [self addChild:sprite]; [movableSprites addObject:sprite]; } [[CCTouchDispatcher sharedDispatcher] addTargetedDelegate:self priority:0 swallowsTouches:YES]; } return self; }
上面两段注释的中文可以基本上解释我们做了些什么。程序首先载入了hud的背景图片,然后我们遍历了一个数组,将这些图像加载并存储为精灵,指定它们的位置,让它们彼此之间保持一定的间距,将其添加为游戏层的子节点,并存储在moveableSprites数组中,便于后续的查找。关于如何把4个图片精确的放在一行,我们还可以参考这里:Ray's tutorials found here about dragging and dropping images.
最后,我们需要通知CCTouchDispatcher,需要处理一些触碰事件。
好了,接下来我们来看看当玩家和游戏层互动的时候,我们该如何处理这些事件:
- (BOOL)ccTouchBegan:(UITouch *)touch withEvent:(UIEvent *)event { CGPoint touchLocation = [self convertTouchToNodeSpace:touch]; CCSprite * newSprite = nil; for (CCSprite *sprite in movableSprites) { if (CGRectContainsPoint(sprite.boundingBox, touchLocation)) { DataModel *m = [DataModel getModel]; m._gestureRecognizer.enabled = NO; selSpriteRange = [CCSprite spriteWithFile:@"Range.png"]; selSpriteRange.scale = 4; [self addChild:selSpriteRange z:-1]; selSpriteRange.position = sprite.position; newSprite = [CCSprite spriteWithTexture:[sprite texture]]; //sprite; newSprite.position = sprite.position; selSprite = newSprite; [self addChild:newSprite]; break; } } return TRUE; }
上面一大堆代码究竟做了些什么?还记得上面提过我们把代表炮塔图片的精灵存储在moveableSprites这个数组中吗?这里我们用一个循环来遍历这些精灵,使用CCRectContainsPoint来判断玩家的触摸点是否在炮塔图片范围内。如果是的话,我们会调用DataModel,通知它首先关闭”gestureRecognizer“,或许你还记得这个变量,它是指向”UIPanGestureRecognizer”的指针,这样做的意思是---当我们在拖动炮塔的时候,先别移动屏幕了。然后我们复制了正在拖动的炮塔,将其赋给selSprite。当然,在此之前,我们还添加了炮塔攻击范围的图片。这样的话当玩家在放置炮塔的时候知道可以打多远。
好了,那么当玩家的触摸点开始移动起来时又会发生什么?让我们来看看ccTouchMoved这个方法:
- (void)ccTouchMoved:(UITouch *)touch withEvent:(UIEvent *)event { CGPoint touchLocation = [self convertTouchToNodeSpace:touch]; CGPoint oldTouchLocation = [touch previousLocationInView:touch.view]; oldTouchLocation = [[CCDirector sharedDirector] convertToGL:oldTouchLocation]; oldTouchLocation = [self convertToNodeSpace:oldTouchLocation]; CGPoint translation = ccpSub(touchLocation, oldTouchLocation); if (selSprite) { CGPoint newPos = ccpAdd(selSprite.position, translation); selSprite.position = newPos; selSpriteRange.position = newPos; DataModel *m = [DataModel getModel]; CGPoint touchLocationInGameLayer = [m._gameLayer convertTouchToNodeSpace:touch]; BOOL isBuildable = [m._gameLayer canBuildOnTilePosition: touchLocationInGameLayer]; if (isBuildable) { selSprite.opacity = 200; } else { selSprite.opacity = 50; } } }
通过上面的代码,我们可以在屏幕上拖动“伪“炮塔,这样玩家就知道在哪里可以放下炮塔。更有趣的是,哥在这里添加了额外的代码(调用TutorialScene.m中canBuildOnTilePosition这个方法),从而允许程序检查瓦片的位置,判断玩家把炮塔所拖到的位置是否是可修建的:
- (BOOL) canBuildOnTilePosition:(CGPoint) pos { CGPoint towerLoc = [self tileCoordForPosition: pos]; int tileGid = [self.background tileGIDAt:towerLoc]; NSDictionary *props = [self.tileMap propertiesForGID:tileGid]; NSString *type = [props valueForKey:@"buildable"]; if([type isEqualToString: @"1"]) { return YES; } return NO; }
该方法会再次检查瓦片的属性,如果是“可修建的”(值为1),就返回YES,否则返回NO。如果该瓦片是不可修建的,我们将CCSprite精灵的透明度设置为50,否则,将其设置为100(完全显示)。当然,你完全可以用其它方法来向玩家显示该处是否可以修建,比如将精灵染成红色代表所在位置不可修建,而染成绿色或蓝色代表所在位置可以修建。这里就不需要多言了,感兴趣的朋友自己来试试吧。
到了现在,就只剩对触摸事件结束的处理了:
- (void)ccTouchEnded:(UITouch *)touch withEvent:(UIEvent *)event { CGPoint touchLocation = [self convertTouchToNodeSpace:touch]; DataModel *m = [DataModel getModel]; if (selSprite) { CGRect backgroundRect = CGRectMake(background.position.x, background.position.y, background.contentSize.width, background.contentSize.height); if (!CGRectContainsPoint(backgroundRect, touchLocation)) { CGPoint touchLocationInGameLayer = [m._gameLayer convertTouchToNodeSpace:touch]; [m._gameLayer addTower: touchLocationInGameLayer]; } [self removeChild:selSprite cleanup:YES]; selSprite = nil; [self removeChild:selSpriteRange cleanup:YES]; selSpriteRange = nil; } m._gestureRecognizer.enabled = YES; }
在放下炮塔之前,得让玩家有后悔的机会,也就是说在某些地方放下炮塔是可以取消修建的。默认的位置当然就是gameHud层本身。所以我们做了一个判断,看触摸点结束的位置是否在Hud背景图片的范围内。如果被确认在游戏层上,我们就调用addTower这个方法。如果不是,我们只需要清除这个炮塔,同时清除“伪”炮塔图片和代表其攻击范围的精灵。
看完这一段,可谓小功告成了!让合作的美工稍作修改,你会发现已经像那么回事了。那么下一章我们要做的就是,把这部分和上部分的内容整合在一起,再加上可以开火的炮塔!
小结一下,
本章我们创建了一个独立的game hud层;
学习了从一个层将炮塔拖曳到另一个层;
学习了瓦片地图的更多技巧!
我们还将学习的是:
如何让炮塔向敌人开火
怎样开启一轮轮的波次进攻,以及如何制作特殊的炮塔!