这篇里面我们开始制作我们的飞船。
首先介绍一个有用的工具,TexturePacker。
首先我们简单介绍一下使用TexturePacker处理图片素材的好处。这里我们就不得不提一下Cocos2d中Texture的处理方式,在Cocos2d中,CCSprite(精灵)对象的纹理的长宽都是2的幂指数,比如一个精灵由一张100×200的图片初始化,那么实际图片占用的内存有多大呢?答案是128×256B即32KB,有一部分空间被浪费掉了。
我们注意到飞船还有子弹等元素都是很小的图片,如果我们一张一张添加到工程Resources中,一张一张的加载这些图片的话,有很多补足的空间都被浪费了,那么有一个解决方法显而易见,就是把这些小图组合到一起,这样这些小图彼此紧密的组合在一起,很好的补充了那些浪费的空间,节约了内存使用,同时,加载这一张大图的速度要比一张一张加载小图的效率要高得多,可以提高程序的执行速度。
那么我们把小图捏到一起之后,怎么在程序中再把这些图片一张张拆出来呢?必然是用一个文件去记录每个图片的位置信息和大小信息,包括图片是否被旋转等,然后在读取图片的时候通过偏移来找到那张需要的图片。
好了,原理大概说完了,这些TexturePacker都替我们做了,我们打开TexturePacker,看到如下的界面:
我们可以拖拽图片文件到最右侧Sprites列表中,图片会按照最优的方式被组合到一起,我们将下面这几张图片拖拽到里面:
bullet.png:
aircraft1.png:
aircraft2.png:
aircraft3.png:
aircraft4.png:
得到的结果如下:
在左侧的Data FileName点击右边的文件选择器,文件名中填入:dark-travel-elements-hd.plist,(这个名字也可以自己起,后面加载的时候,相应的代码需要修改为自己起的名字即可),名字后面的-hd后缀需要保留,TextureFormat选择“GZip.compr.PVR(.pvr.gz, Ver.2)”,勾选Premultiply alpha,防止图片边缘出现黑边。Imageformat选择RGBA4444即可,点击AutoSD右侧的设置按钮,点击Apply生成SD版本的图片(HD和SD版本分别是针对Retina屏和非Retina屏所做的区分,有兴趣的童鞋可以查查资料):
设置完成后,点击上边菜单publish按钮生成4个文件:
dark-travel-elements-hd.plist
dark-travel-elements-hd.pvr.gz
dark-travel-elements.plist
dark-travel-elements.pvr.gz
然后我们把这4个文件加入到我们工程的Resources中。
下面我们开始添加我们的飞船。
在开始写代码之前,我们考虑一下,这个游戏最终是要进行玩家和敌人(电脑)飞船之间对抗的,玩家飞船和敌人飞船有哪些相同的属性呢,是不是能够抽象出相同的地方创建一个父类呢?
答案是肯定的。我们想一下,飞船都有哪些属性和动作呢?作为一个游戏,飞船肯定需要有生命值,有速度等属性吧,那有哪些动作(或者说方法)呢?移动,射击,爆炸这些方法。
我们创建一个新的类作为飞船的父类:Entity继承自CCNode,Entity.h代码如下:
#import"CCNode.h"
#import"cocos2d.h"
#import"TagDefinitions.h"
#import"CommonUtility.h"
@interface Entity: CCNode{
float speed;
float health;
float healthRecord; //for reset
BulletTypes bulletType; //if the entity just has onetype of bullet, use this
CCSprite* entitySprite;
}
@property(readonly) float speed;
@property(readonly) float height;
@property(readonly) float width;
@property(readonly) CGPoint aimPos;
-(void)changePosition:(CGPoint) pos;
-(void)shootBulletAtPosition:(CGPoint) position bulletType:(BulletTypes)bulletType;
-(void)shootBulletAtPosition:(CGPoint) position
atTarget:(CGPoint) target
bulletType:(BulletTypes) bT;
-(void)shootBulletAtPosition:(CGPoint) position
atAngle:(float) angle
bulletType:(BulletTypes) bT;
-(void)moveByX:(float)xLength andY:(float)uLength;
- (void)explode;
@end
Entity.m的代码:
#import"Entity.h"
#import"GameLayer.h"
#import"SimpleAudioEngine.h"
@implementationEntity
-(void)changePosition:(CGPoint) pos{
if (CGRectContainsRect([[CommonUtility utility] screenRect], [entitySpriteboundingBox])) {
float spriteWidth = entitySprite.contentSize.width;
float spriteHeight = entitySprite.contentSize.height;
if (pos.x < 0) {
pos.x = 0;
}
else if (pos.x > [CommonUtility utility].screenWidth - spriteWidth){
pos.x = [CommonUtility utility].screenWidth - spriteWidth;
}
float heightLimit = [CommonUtility utility].screenScale * 100;
if (pos.y < heightLimit) {
pos.y = heightLimit;
}
else if (pos.y > [CommonUtility utility].screenHeight - spriteHeight){
pos.y = [CommonUtility utility].screenHeight - spriteHeight;
}
}
[entitySprite setPosition:pos];
} //changePosition
-(void)moveByX:(float)xLength andY:(float)yLength{
CGPoint movedLocation = ccp(entitySprite.position.x + xLength,entitySprite.position.y + yLength);
[self changePosition:movedLocation];
} //moveByX
-(void)shootBulletAtPosition:(CGPoint) position
bulletType:(BulletTypes) bT{
} //shootBulletAtPosition
-(void)shootBulletAtPosition:(CGPoint) position
atTarget:(CGPoint) target
bulletType:(BulletTypes) bT{
} //shootBulletAtPosition
-(void)shootBulletAtPosition:(CGPoint) position
atAngle:(float) angle
bulletType:(BulletTypes) bT{
} //shootBulletAtPosition
- (float)height{
return [entitySprite contentSize].height;
} //height
- (float)width{
return [entitySprite contentSize].width;
} //width
-(CGPoint)position{
return entitySprite.position;
} //position
- (CGPoint)aimPos{
return ccp(entitySprite.position.x+self.width*0.5f,entitySprite.position.y+self.height*0.5f);
}
- (BOOL)visible{
return entitySprite.visible;
} //visible
- (float)speed{
return speed;
} //speed
- (void)explode{
}
- (void)dealloc{
entitySprite = nil;
[super dealloc];
}
@end
这里我们实现了changePosition和moveByX:andY:这两个方法,这两个方法都是改变Entity位置的,changePosition中控制对象不会飞出屏幕,同时注意一下下面这行代码:
if(CGRectContainsRect([[CommonUtility utility] screenRect], [entitySpriteboundingBox])){…}
我们知道,敌人飞船一开始是不会在屏幕上的,那么他们是怎么出现的呢?突然闪烁出来?也可以其实,不过我个人觉得还是从屏幕右侧慢慢飞出来比较正常,不然敌人就太厉害了哈哈。所以我们将changePosition中的判断逻辑用这个if语句包起来,这样当敌人飞船在屏幕外的时候,就不会执行判断语句导致无法从外面飞进来。
另外在类中我们定义了一些基本属性,这里挑一些不容易理解的解释一下,entitySprite是Entity对应的精灵,屏幕上我们看到的Entity就是这个精灵;另外有个属性需要注意一下,我们定义了一个health,那healthRecord是干啥的呢?想象一下,作为一款飞行射击游戏,是不是需要让玩家看到飞船和敌人的生命值?这就对了,healthRecord记录的就是Entity的原始生命值,health是当前生命值,我们初始化的时候,可以用一个长方形代表Entity的生命值,当生命值改变时,我们用这两个的比例就能够算出这个长方形的长度来,也就是飞船或者敌人的剩余生命值。
另外,我们声明了三个射击方法:
-(void)shootBulletAtPosition:bulletType:
-(void)shootBulletAtPosition:atTarget:bulletType:
-(void)shootBulletAtPosition:atAngle:bulletType:
三个方法都允许指定子弹类型,第一个方法允许飞船射出水平的子弹,第二个方法允许飞船向指定目标射击,第三个方法允许飞船将子弹以一定角度射出。我们目前还没有子弹,所以暂时将这三个方法留空。
好了,父类定义好了,我们先来定义玩家的飞船,创建AirCraft类,继承Entity类,AirCraft.h定义如下:
#import"CCNode.h"
#import"Entity.h"
@interfaceAirCraft : Entity{
NSMutableArray* gunPositions;
}
+ (id)airCraft;
+(AirCraft*)sharedAirCraft;
-(void)shootBulletAtGunLevel:(int)gunLevel;
-(void)resetHealth:(float) health;
@end
AirCraft.m代码如下:
#import"AirCraft.h"
#import"cocos2d.h"
#import"TagDefinitions.h"
@implementationAirCraft
static AirCraft*sharedAirCraftData;
+(AirCraft*)sharedAirCraft{
if (sharedAirCraftData == nil) {
sharedAirCraftData =[[[AirCraft alloc] init] autorelease];
}
return sharedAirCraftData;
}
+ (id)airCraft{
return [[[AirCraft alloc] init] autorelease];
}
- (id)init{
if (self = [super init]) {
gunPositions = [[NSMutableArray arrayWithObjects:[NSNumbernumberWithFloat:0.594937], [NSNumber numberWithFloat:0.651899], [NSNumbernumberWithFloat:0.241379], [NSNumber numberWithFloat:0.37931], [NSNumber numberWithFloat:0.741379],[NSNumber numberWithFloat:0.810345], nil] retain];
speed = 300;
health = 1000;
healthRecord = health;
entitySprite = [CCSprite spriteWithSpriteFrameName:@"aircraft1.png"];
CCAnimation* animation = [CCAnimationanimateWithFrameAndRollback:@"aircraft" frameCount:4 delay:0.03fstartPos:1];
CCAnimate* animate = [CCAnimate actionWithAnimation:animation];
CCRepeatForever* repeatAction = [CCRepeatForever actionWithAction:animate];
entitySprite.anchorPoint = CGPointZero;
entitySprite.position = ccp(100, [CommonUtility utility].screenWidth*0.5f-100);
[entitySprite runAction:repeatAction];
[self addChild:entitySprite z:100 tag:AirCraftTag];
}
return self;
}
-(CGPoint)gunPositionAtIndex:(int) gunIndex{
float x;
if (gunIndex == 2 || gunIndex == 3) {
x = [[gunPositions objectAtIndex:1] floatValue];
}else{
x = [[gunPositions objectAtIndex:0] floatValue];
}
float y = [[gunPositions objectAtIndex:gunIndex + 1] floatValue];
return ccp(entitySprite.position.x + entitySprite.contentSize.width * x,entitySprite.position.y + entitySprite.contentSize.height * y);
}
-(void)shootBulletAtGunLevel:(int)gunLevel{
int minLevel = 1;
int maxLevel = 2;
if (gunLevel < minLevel) {
gunLevel = minLevel;
}
else if (gunLevel > maxLevel){
gunLevel = maxLevel;
}
if (gunLevel == minLevel) {
[self shootBulletAtPosition:[self gunPositionAtIndex:2]bulletType:SmallRoundBullet];
[self shootBulletAtPosition:[self gunPositionAtIndex:3]bulletType:SmallRoundBullet];
}
else if (gunLevel == maxLevel) {
[self shootBulletAtPosition:[self gunPositionAtIndex:1]bulletType:PoweredBullet];
[self shootBulletAtPosition:[self gunPositionAtIndex:2]bulletType:PoweredBullet];
[self shootBulletAtPosition:[self gunPositionAtIndex:3]bulletType:PoweredBullet];
[self shootBulletAtPosition:[self gunPositionAtIndex:4]bulletType:PoweredBullet];
}
}
-(void)resetHealth:(float) h{
health = h;
healthRecord = h;
}
- (void)dealloc{
sharedAirCraftData = nil;
[gunPositions removeAllObjects];
[gunPositions release];
[super dealloc];
}
@end
这里我们首先跳过init方法,先说明一下其他变量和方法,我们保存了一个gunPositions数组,这个是用来做什么的呢?我们观察飞船图片,我们给飞船画了4个子弹发射装置(早知道这么麻烦,当初就画一个了T_T),所以我们一共有4个位置发射子弹,而不是从飞船的头部发射,gunPositions定义了这4个发射装置在飞船上的位置,通过他们,我们能够控制子弹精确地从这些位置发射出来(说白了就是设置子弹的初始位置在这些地方)。通过init方法初始化好(这些位置是写死的,如果换飞船图片的话,这些位置就失效了),通过gunPositionAtIndex来获取每个位置。另外,方法shootBulletAtGunLevel是做什么的呢?为了增加游戏性,我们定义飞船可以发射两种子弹,一种是高杀伤的子弹,一种是频率快的子弹,玩家可以根据不同类型的敌人发射不同的子弹,因此,我们通过这个方法来决定玩家发射子弹的数量和类型,发射频率后面会添加控制逻辑,各位稍安勿躁。另外实现了resetHealth并且重载了dealloc方法,这里不多做说明。
好了,我们回头看看init方法,首先是一些初始化的语句,这里不多说,我们看后面CCAnimation的这些语句:
CCAnimation*animation = [CCAnimation animateWithFrameAndRollback:@"aircraft"frameCount:4 delay:0.03f startPos:1];
CCAnimate* animate = [CCAnimate actionWithAnimation:animation];
CCRepeatForever* repeatAction = [CCRepeatForever actionWithAction:animate];
entitySprite.anchorPoint = CGPointZero;
entitySprite.position = ccp(100, [CommonUtility utility].screenWidth*0.5f-100);
[entitySprite runAction:repeatAction];
首先你会发现编译器提示你找不到animateWithFrameAndRollback方法,你会发现CCAnimation类里面没有这个方法,不卖关子了,这是我们利用object-c的Category特性对CCAnimation类进行扩展的方法,这个扩展类我们稍后奉上,首先先看下这几行代码,第一行我们定义一个动画效果,动画由aircraft1/2/3/4.png组成,实现了飞船后面的喷气效果,第二句将CCAnimation转换为一个Action对象,然后我们定义一个死循环的动画Action(CCRepeatForever),这个Action会一直重复播放初始化的animate这个动画效果,然后我们用CCSprite的runAction来播放这个循环动画效果,这样就实现了飞船的喷气效果。
下面我们补上CCAnimation的扩展类:
CCAnimation+Helper.h:
#import"CCAnimation.h"
#import"cocos2d.h"
@interfaceCCAnimation (Helper)
+(CCAnimation*)animateWithFrame:(NSString*) frame
frameCount:(int) frameCount
delay:(float)delay
startPos:(int) startPos;
+(CCAnimation*)animateWithFrameAndRollback:(NSString*) frame
frameCount:(int) frameCount
delay:(float) delay
startPos:(int) startPos;
@end
CCAnimation+Helper.m:
#import"CCAnimation+Helper.h"
@implementationCCAnimation (Helper)
+(CCAnimation*)animateWithFrame:(NSString *)frame
frameCount:(int)frameCount
delay:(float)delay
startPos:(int) startPos{
NSMutableArray* frames = [NSMutableArray arrayWithCapacity:frameCount];
CCSpriteFrameCache* frameCache = [CCSpriteFrameCache sharedSpriteFrameCache];
for (int i = 0; i < frameCount; i++) {
NSString* file = [NSString stringWithFormat:@"%@%i.png", frame,i+startPos];
CCSpriteFrame* frame = [frameCache spriteFrameByName:file];
[frames addObject:frame];
}
return [CCAnimation animationWithSpriteFrames:frames delay:delay];
}
+(CCAnimation*)animateWithFrameAndRollback:(NSString*) frame
frameCount:(int) frameCount
delay:(float) delay
startPos:(int) startPos{
NSMutableArray* frames = [NSMutableArray arrayWithCapacity:frameCount];
CCSpriteFrameCache* frameCache = [CCSpriteFrameCache sharedSpriteFrameCache];
for (int i = 0; i < frameCount; i++) {
NSString* file = [NSString stringWithFormat:@"%@%i.png", frame,i+startPos];
CCSpriteFrame* frame = [frameCache spriteFrameByName:file];
[frames addObject:frame];
}
for (int i = frameCount-2; i > -1; i--) {
NSString* file = [NSString stringWithFormat:@"%@%i.png", frame,i+startPos];
CCSpriteFrame* frame = [frameCache spriteFrameByName:file];
[frames addObject:frame];
}
return [CCAnimation animationWithSpriteFrames:frames delay:delay];
}
@end
这里解释一下下面这个方法:
(CCAnimation*)animateWithFrame:frameCount:delay:startPos:
我们在命名动画的每一帧图片的时候,按照动画帧的播放顺序定义png文件名,如aircraft1.png —aircraft4.png,因此在添加CCSpriteFrame的时候,我们可以提供一个文件名,如“aircraft”,提供一个start值和count值,然后用一个循环把1— 4这4张图片加载到数组中,然后从用数组创建CCAnimation动画。另一个方法定义了创建循环动画的方法,这里不再赘述了。这里用到了一个类:
CCSpriteFrameCache*frameCache = [CCSpriteFrameCache sharedSpriteFrameCache];
我们需要在GameLayer中将我们用TextPacker创建的那个组装好的plist文件load到这个Cache中,我们在GameLayer中添加如下方法:
-(void)initCaches{
CCSpriteFrameCache* frameCache = [CCSpriteFrameCache sharedSpriteFrameCache];
[frameCache addSpriteFramesWithFile:@"dark-travel-elements.plist"];
}
然后我们在GameLayer的init方法中添加对这个方法的调用:
- (id)init{
if (self = [super init]) {
[self initCaches];
[self initBackground];
[self scheduleUpdate];
}
return self;
}
好了,貌似大功告成了,编译,运行。
哈哈,是不是和之前一样,只有背景?那就对了,我们还没把飞船加到场景中呢。在GameLayer的init方法中scheduleUpdate那行的上面加上下面的代码:
AirCraft* airCraft= [AirCraft sharedAirCraft];
[selfaddChild:airCraft z:3 tag:AirCraftTag];
这样飞船就被加入到场景中了,现在运行一下看看,飞船已经出现在屏幕上了,再仔细观察飞船尾部,可以看到喷气动画了吧,效果如下:
好了,今天先到这儿,下一篇我们将使用开源代码为飞船加上摇杆控制,敬请期待~