【原】使用Cocos2d制作一个类似于魔塔的iPhone游戏-第一部分(上)
这是我在博客园的第一篇教程,因为本人也仅仅是初二学生,在很多方面都比不上大哥大姐们,这篇教程如果有不足的地方也请大家多多指出,希望可以帮助到一些新手。
通过这系列教程,你将可以制作出一个类似于魔塔的游戏。如果曾经玩过这个Flash小游戏,那么当然最好,如果你没有玩过,也不用担心,你可以点击这里来玩这个游戏,以便于了解我们接下来该制作的游戏的大致效果。
教程图片:
你可以在GitHub上面下载到这个项目的完整源代码:https://github.com/hxy060799/MagicTower/
这个教程将会分为三个部分,因为时间有限,所以不能一次性把所有的三篇教程一起写完,希望大家可以谅解。
在这个教程中,我们每一个部分都将实现不同的目标:
·在第一篇教程中,你将会学会如何用Cocos2d画出一个二维地图,并且如何实现地图中每一个方块的动画。
·在第二篇教程中,你将会学会如何添加一个玩家方块以及如何让玩家和怪物进行战斗并且拾取一些装备。
·在第三篇教程中,你将会学会如何在游戏中添加剧情并且实现物品的交易等功能。
从这里开始
首先,你应该已经在你的xCode中安装好了Cocos2d模板。这篇教程不应该是你学习Cocos2d的入门教程,而应该是略带提高的教程。
那么我们开始吧,打开xCode,新建一个项目,选择Cocos2d iOS模板,这个应用中我们不需要物理引擎。我们把这个项目命名为MagicTowerTutorial,平台选择iPhone。
创建好了项目,尝试运行,可以看到默认的模板所带有的HelloWorld界面。
为了方便辨识,我们需要把模板创建的HelloWorldLayer类的名称改为GameLayer,使用xCode的查找面板可以很容易地完成这个操作。
完成以后,手动把HelloWorldLayer.h和HelloWorldLayer.m重命名为GameLayer.h和GameLayer.m。这时应该可以编译通过了。
制作这个游戏我们需要一些图片和资源,你们很幸运。因为我已经将需要的资源都已经打包准备好了。请从这里下载资源包。
下载好资源包,我们首先先理解里面的资源都是做什么用的:
·MagicTowerArt:里面包含游戏中所用到的全部图片,它们已经用TexturePacker包装好了。
·Documents:从原版魔塔里面提取出来的怪物信息、物品信息报表、剧情等信息,在程序中不会被使用到,但是在开发过程中将会基于一定的帮助。
·buttons:程序中用到的按钮
·OtherResources:包括地图信息、方块信息、商店信息、对话信息等。
·因为制作这个游戏的时候时间比较赶,所以游戏中没有使用音频。
我们首先添加一个背景图片,将资源包中MagicTowerArt/background/gameBackground.png添加到工程里面,其他资源先丢在一边。
接下来,要修改模板创建的代码。将GameLayer.m的init中的代码全部删掉,用下面的代码替换。
-(id) init { if(self=[super init]){ //添加背景 CCSprite *backgroundSprite=[[CCSprite alloc]initWithFile:@"gameBackground.png"]; backgroundSprite.anchorPoint=ccp(0,0); [self addChild:backgroundSprite z:0]; [backgroundSprite release]; } return self; }
尝试运行,你可以看到一个紫色的背景。
准备地图数据
接下来,我们就需要对地图进行制作。魔塔游戏一共有21层,加上一个序章,一共有22层,再补上对所有地图方块进行测试的层,应该有23层。每一层都是由11*11的正方形方块组成。我们可以借助一个三维数组表示这样的地图,而每一个点的方块可以用int来表示,每个数字都表示一种方块类型,大家可以通过下面的图片来理解。
通过Plist可以记录这样的地图信息。我已经把相关的地图信息全部记录在了资源包OtherResources中的MapInformation.plist里面的LevelMap数组里面了。
把这个plist文件加入到工程里面,然后我们需要将这个plist中的内容读取到一个NSMutableArray中,通过下面的代码可以完成
在GameLayer.m的上面加入
@interface GameLayer(){ //游戏地图,三位数组[楼层,y,x](x<11,y<11) NSMutableArray *gameMap; } @end
因为这个地图数据对象不需要被别的类访问,所以定义为一个私有对象就可以了。
我们可以通过下面的代码读取一个Plist文件中的数据:
-(NSMutableDictionary*)readPlistWithPlistName:(NSString*)plistName{ NSString *error=nil; NSPropertyListFormat format; NSMutableDictionary *dict=nil; NSString *filePath=[[NSBundle mainBundle] pathForResource:plistName ofType:@"plist"]; NSData *plistXML=[[NSFileManager defaultManager]contentsAtPath:filePath]; dict=(NSMutableDictionary*)[NSPropertyListSerialization propertyListFromData:plistXML mutabilityOption:NSPropertyListMutableContainersAndLeaves format:&format errorDescription:&error]; return dict; }
尝试在init中加入:
gameMap=[[NSMutableArray alloc]initWithArray:[[self readPlistWithPlistName:@"MapInformation"]objectForKey:@"LevelMap"]]; NSLog(@"%@",gameMap);
在dealloc中加入:
[gameMap release];
运行后便可以看到程序输出了带有大量int数据的三位数组,这个便是我们的地图数据。
对于获取第Z层第Y行第X列的方块,我们可以用下面的代码来获取:
[[[gameMap objectAtIndex:z]objectAtIndex:y]objectAtIndex:x];
因为这里的地图数据是用int表示的,所以光知道每个位置的方块上的int还不能够画出地图,还必须知道这个int所对应的方块的图片等信息,所以我有准备了一个plist,叫做blockInformation.plist和mapInformation.plist在同一位置,用于表示每个方块数字对应的相关信息。
将这个plist也加入到工程中,以一维数组的方式记录每个方块的信息,里面主要包括这样一些信息:
·ID:用于表示这个方块的名称,以方便在编辑这个信息表的时候进行区分,在程序中不会被用到。
·Type:方块的类型(物品、墙壁、怪物还是空气),它在后面用来决定玩家是要捡起它、被它挡住、走上前攻打它还是走上前。
·UsingImage:方块对应的图像
·AnimationID:方块对应的动画的标识,之后会被用到。
之后,我们也需要将方块信息准备到程序中,在程序中加入:
//在gameMap声明下方加入 //用于储存各种方块的信息 NSMutableArray *blocksInformation; //在init中加入 blocksInformation=[[NSMutableArray alloc]initWithArray:[[self readPlistWithPlistName:@"BlockInformation"]objectForKey:@"BlockInformation"]]; NSLog(@"%@",blocksInformation); //在Dealloc中加入 [blocksInformation release];
到此为止,地图数据就已经准备完毕了,接下来就可以尝试把地图画出来了。
用精灵(CCSprite)画出地图
尽管游戏共有11层,但是我们只需要11*11=121个精灵(CCSprite)来画出这样的地图。如果楼层切换掉,则对这些进行修改来匹配新的方块。
所以,我们需要将这些精灵管理在一个二维数组里面,所以:
//在blocksInformation声明下方加入 //地图是通过11*11=121个精灵拼凑而成的,这是个二维数组 NSMutableArray *mapSprites; //在Dealloc中加入 [mapSprites release];
这里需要注明的是,如果要获取Y行X列的精灵,则需要通过下方的代码来获取。
[[mapSpritesobjectAtIndex:Y]objectAtIndex:X];
我们还需要一个方法来初始化所有的sprites:
-(void)initMapSprites{ mapSprites=[[NSMutableArray alloc]init]; for(int i=0;i<11;i++){ NSMutableArray *rowSprites=[[NSMutableArray alloc]init]; for(int j=0;j<11;j++){ CCSprite *mapBlockSprite=[[[CCSprite alloc]init]autorelease]; mapBlockSprite.anchorPoint=ccp(0,0); mapBlockSprite.position=ccp(103+25*j,23+25*i); [self addChild:mapBlockSprite z:2]; [rowSprites addObject:mapBlockSprite]; } [mapSprites addObject:rowSprites]; } }
上面的代码不难理解,我们将这些精灵初始化,并且按顺序摆放好。这里每个精灵的长宽也就是方块的长宽都是25px,为了让地图显示在正中间,所以将它的起始x加上了103px,y加上了23px。需要注意的Cocos2d坐标原点在左下角,和UI的左上角原点位置不同。在init中调用这个方法。
//加载sprites [self initMapSprites];
编译运行,除了背景什么都看不到,因为所有的精灵对应的texture(图像)还是空的,但精灵确实已经被加载出来了。
下面,我们就真正让这些方块以图像的方式显示出来。在资源包里面,我已经准备好了所有需要使用的图片。把block_items.plist、block_items.png、block_monster.plist、block_monster.png、block_world.plist、block_world.png、block_person.plist、block_person.png、block_doors.plist、block_doors.png全部加入到工程当中。他们都是由texture packer打包好的图像。
因为这些图像使用的是SpriteFrames的形式,所以使用之前必须先缓加载这些图像。(如果看过其他cocos2d教程应该可以理解)。在init中加入:
//加载图像 [[CCSpriteFrameCache sharedSpriteFrameCache]addSpriteFramesWithFile:@"block_world.plist"]; [[CCSpriteFrameCache sharedSpriteFrameCache]addSpriteFramesWithFile:@"block_monster.plist"]; [[CCSpriteFrameCache sharedSpriteFrameCache]addSpriteFramesWithFile:@"block_items.plist"]; [[CCSpriteFrameCache sharedSpriteFrameCache]addSpriteFramesWithFile:@"block_doors.plist"]; [[CCSpriteFrameCache sharedSpriteFrameCache]addSpriteFramesWithFile:@"block_person.plist"];
然后,再加入用来加载对应图片的方法:
-(void)loadSpritesWithFloor:(int)floor{ for(int i=0;i<11;i++){ for(int j=0;j<11;j++){ [self reloadMapBlockWithX:j Y:i Floor:floor]; } } } -(void)reloadMapBlockWithX:(int)bX Y:(int)bY Floor:(int)floor{ CCSprite *spriteToReload=[[mapSprites objectAtIndex:bY]objectAtIndex:bX]; int thisBlock=[self blockAtFloor:floor X:bX Y:bY]; NSDictionary *currentBlockInformation=[blocksInformation objectAtIndex:thisBlock]; NSString *fileName=[currentBlockInformation valueForKey:@"UsingImage"]; CCSpriteFrame* myFrame = [[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName:fileName]; [spriteToReload setDisplayFrame:myFrame]; } -(int)blockAtFloor:(int)floor X:(int)x Y:(int)y{ return [[[[gameMap objectAtIndex:floor]objectAtIndex:y]objectAtIndex:x]intValue]%100; }
直接看这三个方法,估计很多人会看不懂,这里作解释。第一个方法中,进行了一个遍历,对每一个方块都使用第二个方法加载图片,第二个方法中,thisBlock表示这个方块的int值,再使用这个int值调出之前准备好的blocksInformations中的“UsingImage”信息,即使用的图片,然后加载这个图片并且让精灵套用这个图片。第三个方法用于返回指定楼层、行、列的方块数值(上面已经说过),后面的和100取余是因为后面为了写剧情模块的需要而在部分的两位数方块数字前面加上了一个百位数用来做附加值,这里可以不用去纠结它,后面会讲到。
注意一下,很多人会在这部分的实现方法上首先考虑在切换楼层的时候对所有精灵进行销毁然后重新加载一批,这样不仅浪费资源,而且很容易造成内存泄漏。所以,如果碰到类似这样的大量精灵需要加载,应该把精灵的alloc init 和图像的设置分开来写。
激动人心的时刻到了,再init中加入:
[self loadSpritesWithFloor:0];
然后运行程序,第一层地图已经被画出来了。
我们需要两个按钮来进行楼层的上下切换,所以把资源包中的buttons中的文件里面的按钮加入到工程中,然后加入如下代码:
//在声明部分中加入 //当前楼层 int currentFloor; //在init中加入 currentFloor=0; //添加按钮 CCMenuItem *plusButton=[CCMenuItemImage itemWithNormalImage:@"ButtonPlus.png" selectedImage:@"ButtonPlusSel.png" block:^(id sender){ currentFloor+=1;[self loadSpritesWithFloor:currentFloor]; }]; plusButton.anchorPoint=ccp(0,0); CCMenuItem *minusButton=[CCMenuItemImage itemWithNormalImage:@"ButtonMinus.png" selectedImage:@"ButtonMinusSel.png" block:^(id sender){ currentFloor-=1;[self loadSpritesWithFloor:currentFloor]; }]; minusButton.anchorPoint=ccp(0,0); minusButton.position=ccp(0,35); CCMenu *buttonMenu=[CCMenu menuWithItems:plusButton,minusButton,nil]; buttonMenu.anchorPoint=ccp(0,0); [buttonMenu setPosition:ccp(0,0)]; [self addChild:buttonMenu z:100];
再次编译运行,左下角多了用于控制楼层的按钮。原理很简单,当按钮被点按的时候,对当前楼层进行操作,然后通过当前楼层重新加载这些sprites(下图是第三层)。
何去何从
今天的教程就先讲到这里吧,接下来的第一部分下将会告诉大家如何给这些sprites加入动画。希望大家在看完这个教程之后有所收获。我也快困死了,白天刚刚开完运动会六点多起床晚上在这里写教程写到接近十点半,也真心希望大家能够喜欢这份教程。如果需要的话,可以在这里下载到这个教程到目前为止的工程。
这是我的第一篇文章。真心希望大家能够支持并且能够从中学到一些什么。
不见不散。