我们接着上一篇教程,继续开发我们的游戏。
本篇教程我们在之前的基础上添加子弹,然后用之前创建的按钮控制飞船发射子弹。
首先介绍一下CCSpriteBatchNode类,我们知道,在射击类游戏中,我们发射的,敌人发射的子弹有很多很多,相同类型的子弹长得都一样,也就是使用的是相同的图片纹理,正常的情况是,我们发射一颗子弹,程序准备渲染,渲染图形,子弹消失后释放资源。这样在游戏过程中就会不断的重复这个过程。那么我们是否能够为相同的子弹使用相同的图片纹理,这样在程序的执行过程中只会有一次准备和释放过程呢?答案是肯定的。我们使用CCSpriteBatchNode为CCSprite提供相同的纹理,使用CCSpriteBatchNode的前提条件是所有CCSprite都必须与CCSpriteBatchNode使用相同的纹理。
首先我们定义Bullet类,Bullet.h文件:
#import"CCNode.h"
#import"cocos2d.h"
@interface Bullet: CCSprite{
CGPoint speed;
float damage;
float angle;
CGPoint targetPos;
}
-(id)initWithFrameName:(NSString*) frameName
andSpeed:(CGPoint) speed
andDamage:(float) dmg;
-(void)initByPos:(CGPoint) pos;
-(void)initByPos:(CGPoint) pos
andTarget:(CGPoint) tp;
-(void)initByPos:(CGPoint) pos
andAngle:(float) angle;
- (void)recollect;
@property(readonly) float damage;
@end
bullet.m文件:
#import"Bullet.h"
#import"CommonUtility.h"
#define PI3.141592653
@implementationBullet
-(id)initWithFrameName:(NSString*) frameName
andSpeed:(CGPoint) s
andDamage:(float)dmg{
if (self = [super initWithSpriteFrameName:frameName]) {
self.anchorPoint = CGPointZero;
speed = s;
damage = dmg;
self.visible = NO;
}
return self;
} //initWithFrameName
-(void)initByPos:(CGPoint)pos{
self.position = pos;
self.visible = YES;
targetPos = CGPointZero;
[self scheduleUpdate];
} //initByPos
-(void)initByPos:(CGPoint) pos
andTarget:(CGPoint) tp{
self.position = pos;
self.visible = YES;
if (self.position.x < tp.x + 100) {
tp.x = self.position.x - 100;
}
targetPos = tp;
[self scheduleUpdate];
} //initByPos
-(void)initByPos:(CGPoint) pos
andAngle:(float) a{
self.position = pos;
self.visible = YES;
angle = a;
[self scheduleUpdate];
} //initByPos
-(void)update:(ccTime)delta{
float xMove = delta * speed.x;
float yMove = delta * speed.y;
if (targetPos.x != 0 && targetPos.y != 0 && self.position.x !=targetPos.x) {
float a = atanf((self.position.y - targetPos.y) / (self.position.x -targetPos.x));
yMove = xMove * sinf(a);
xMove = xMove * cosf(a);
}else if (angle != -1000){
float a = 2 * PI * angle / 360;
xMove = delta * speed.x * cosf(a);
yMove = delta * speed.x * sinf(a);
}
CGPoint pos = ccp(self.position.x + xMove, self.position.y + yMove);
[self setPosition:pos];
if (CGRectIntersectsRect([self boundingBox], [CommonUtilityutility].screenRect) == NO) {
[self recollect];
}
} //update
- (void)recollect{
self.visible = NO;
angle = -1000;
targetPos = CGPointZero;
[self unscheduleAllSelectors];
} //recollect
- (float)damage{
return damage;
} //damage
- (void)dealloc{
[super dealloc];
} //dealloc
@end
首先解释一下,我们为什么要继承CCSprite呢,一个原因是CCSpriteBatchNode的addChild方法只能添加CCSprite或者其子类,当然这不是主要原因,另一个原因是子弹类本身也不需要包含太复杂的逻辑,我们可以认为它就是界面的一个小元素。当然,一种更合理的模式是我们的子弹类继承CCNode,CCSprite作为类的一个属性,而不是类本身,在利用CCSpriteBatchNode的时候,将子弹类的CCSprite属性添加到CCSpriteBatchNode中,后面我们在添加敌人的时候用的就是这种模式。
书归正传,我们定义了子弹的几个属性:速度、伤害(不能是糖衣炮弹)、角度(angle)和目标(target)。这里解释下后面两个属性,还记得我们的Entity的射击的三个方法么:水平射击,按一定角度射击,向指定目标射击,其实我们的飞船只是将子弹以一定的参数初始化好,然后子弹自己按照参数飞出去直到飞出屏幕或者击中目标。所以子弹的后两个属性就对应了Entity的后两种射击方式。
接着我们看一下方法,三种初始化方法,分别对应射击的三个方法:初始化angle,初始化target,只初始化基本属性。在update方法中,根据angle和target是否被初始化来判断子弹如何飞行,如果子弹飞出屏幕,将其回收(也就是隐藏起来以备下一次利用,这里注意,我们回收并没有释放子弹这个类)。
接着我们定义一个子弹的Cache类,这里我们考虑一下,一个CCSpriteBatchNode对应一张纹理贴图,所以我们有两种方式来处理各种各样的BulletCache:一种是每个CCSpriteBatchNode处理一个子弹纹理,然后再用一个类管理各种CCSpriteBatchNode;另一种方式是使用TexturePacker将各种子弹组装成一张大纹理,然后用一个CCSpriteBatchNode来管理。这里第二种方式的好处是可以把子弹都几种到一张纹理图中,但是问题是在获取某种子弹精灵的时候,要进行更多的计算,代码的可读性比较差,所以我们选择第一种方式来做。除了之前我们做过的两种子弹,我们又为敌人设计了几种子弹(虽然还没有敌人,未雨绸缪嘛),各种子弹图片如下:
bigenemybullet.png:
bossbullet.png:
bullet.png:
poweredenemybullet.png:
roundBullets.png:
smallenemybullet.png:
我们先不管敌人的这几种子弹,我们先把Cache类写好,后面我们添加敌人的飞船的时候,再把这些子弹和敌人飞船贴图整合到一起。我们首先添加BulletCache类,继承自CCNode,BulletCache.h定义如下:
#import<Foundation/Foundation.h>
#import"cocos2d.h"
#import"Bullet.h"
#import"TagDefinitions.h"
@interfaceBulletCache : CCNode{
int nextIdleBulletCachePosData;
CCSpriteBatchNode* bulletCache;
}
-(id)initWithFrameName:(NSString*) frameName
andBulletCacheCount:(int) cacheCount
andBulletSpeed:(CGPoint) speed
andDamage:(float) dmg;
-(Bullet*)nextIdleBullet;
-(float)getBulletHitDamageWithinArea:(CGRect) area;
@property(readonly) CCSpriteBatchNode* bullets;
@property intnextIdleBulletCachePos;
@end
BulletCache.m代码如下:
#import"BulletCache.h"
@implementationBulletCache
-(id)initWithFrameName:(NSString*) frameName
andBulletCacheCount:(int) cacheCount
andBulletSpeed:(CGPoint)speed
andDamage:(float)dmg{
if (self = [super init]) {
nextIdleBulletCachePosData = 0;
CCSpriteFrameCache* frameCache = [CCSpriteFrameCache sharedSpriteFrameCache];
CCSpriteFrame* frame = [frameCache spriteFrameByName:frameName];
bulletCache = [CCSpriteBatchNode batchNodeWithTexture:frame.texture];
for (int i = 0; i < cacheCount; i++) {
Bullet* bCache = [[[Bullet alloc]initWithFrameName:frameName andSpeed:speedandDamage:dmg] autorelease];
[bulletCache addChild:bCache];
}
}
return self;
} //initWithFrameName
-(Bullet*)nextIdleBullet{
return [[bulletCache children] objectAtIndex:self.nextIdleBulletCachePos];
} //nextIdleBullet
-(int)nextIdleBulletCachePos{
int tempId = nextIdleBulletCachePosData;
nextIdleBulletCachePosData++;
if (nextIdleBulletCachePosData >= [[bulletCache children] count]) {
nextIdleBulletCachePosData = 0;
}
return tempId;
} //nextIdleBulletCachePos
-(void)setNextIdleBulletCachePos:(int)nextIdleBulletCachePos{
nextIdleBulletCachePosData = nextIdleBulletCachePos;
} //setNextIdleBulletCachePos
-(CCSpriteBatchNode*)bullets{
return bulletCache;
} //bullets
-(float)getBulletHitDamageWithinArea:(CGRect) area{
float damage = 0;
for (Bullet* bullet in [bulletCache children]) {
if (bullet.visible && CGRectIntersectsRect([bullet boundingBox], area)!= NO) {
damage += bullet.damage;
[bullet recollect];
}
}
return damage;
} //getBulletHitDamageWithinArea
- (void)dealloc{
[super dealloc];
} //dealloc
@end
属性nextIdleBulletCachePosData用来记录下一个未被使用的Cache,这个值随着Cache被使用而增加,当达到Cache的总个数的时候,归零。初始化方法中,根据子弹属性和Cache总数,利用一个循环初始化CCSpriteBatchNode。nextIdleBullet返回下一个未使用的Bullet(由于Bullet是依次取出的,所以实际上就是下一个Bullet作为未使用的Bullet),通过nextIdleBulletCachePos属性控制这个计数增长。CCSpriteBatchNode需要添加到层中才能被正确显示(之前在调试程序的时候,没有把这个节点添加到Layer中,只是添加到父节点中,结果子弹怎么也显示不出来,查资料才知道,坑啊T_T),所以我们添加了一个属性:bullets,用来返回CCSpriteBatchNode。getBulletHitDamageWithinArea这个方法简单说明一下,这个方法用来判断一共有多少子弹(当前这种Cache类型)击中指定区域(参数),计算并返回伤害,然后回收这些击中这个区域的子弹。这个方法后面我们用来计算伤害。
这样我们就完成了一种子弹Cache的封装,接着我们添加一个类管理多种Cache:
BulletCacheManager.h代码如下
#import"CCNode.h"
#import"TagDefinitions.h"
#import"cocos2d.h"
#import"Bullet.h"
#import"BulletCache.h"
@interfaceBulletCacheManager : CCNode{
NSMutableDictionary* bulletCacheMapData;
}
+(BulletCacheManager*) sharedBulletCacheManager;
-(Bullet*)getBullet:(BulletTypes) bulletType;
-(void)addBulletCacheWithType:(BulletTypes) bulletType
bulletCacheCount:(int) bulletCacheCount
bulletFrameName:(NSString*) bulletFrameName
bulletSpeed:(CGPoint) speed
bulletDamage:(float) dmg;
-(float)getBulletHitDamageWithinArea:(CGRect) area isPlayerBullet:(BOOL)isPlayerBullet;
@property(readonly) NSMutableDictionary* bulletCacheMap;
@end
BulletCacheManager.m代码:
#import"BulletCacheManager.h"
@implementationBulletCacheManager
staticBulletCacheManager* sharedBulletCache;
+(BulletCacheManager*) sharedBulletCacheManager{
if (sharedBulletCache == nil) {
sharedBulletCache = [[[BulletCacheManager alloc] init] autorelease];
}
return sharedBulletCache;
} //sharedBulletCacheManager
- (id)init{
if (self = [super init]) {
bulletCacheMapData = [[NSMutableDictionary dictionaryWithCapacity:5] retain];
[self addBulletCacheWithType:PoweredBullet bulletCacheCount:100bulletFrameName:@"bullet.png" bulletSpeed:CGPointMake(500, 0)bulletDamage:80];
[self addBulletCacheWithType:SmallRoundBullet bulletCacheCount:300bulletFrameName:@"roundBullets.png" bulletSpeed:CGPointMake(800, 0)bulletDamage:40];
}
return self;
} //init
-(void)addBulletCacheWithType:(BulletTypes) bulletType
bulletCacheCount:(int) bulletCacheCount
bulletFrameName:(NSString*) bulletFrameName
bulletSpeed:(CGPoint) speed
bulletDamage:(float) dmg{
BulletCache* bulletCache = [[[BulletCache alloc]initWithFrameName:bulletFrameName andBulletCacheCount:bulletCacheCountandBulletSpeed:speed andDamage:dmg] autorelease];
[self.bulletCacheMap setObject:bulletCache forKey:[NSNumbernumberWithInt:bulletType]];
[self addChild:bulletCache];
} //addBulletCacheWithType
-(Bullet*)getBullet:(BulletTypes) bulletType{
BulletCache* bulletCache = [self getBulletCache:bulletType];
return [bulletCache nextIdleBullet];
} //getBullet
-(BulletCache*)getBulletCache:(BulletTypes) bulletType{
return [self.bulletCacheMap objectForKey:[NSNumber numberWithInt:bulletType]];
}
-(NSMutableDictionary*)bulletCacheMap{
return bulletCacheMapData;
} //bulletCacheMap
-(float)getBulletHitDamageWithinArea:(CGRect) area isPlayerBullet:(BOOL)isPlayerBullet{
float damage = 0;
if (isPlayerBullet) {
damage += [[self getBulletCache:PoweredBullet]getBulletHitDamageWithinArea:area];
damage += [[self getBulletCache:SmallRoundBullet]getBulletHitDamageWithinArea:area];
}else{
damage += [[self getBulletCache:SmallEnemyBullet]getBulletHitDamageWithinArea:area];
damage += [[self getBulletCache:BigEnemyBullet]getBulletHitDamageWithinArea:area];
damage += [[self getBulletCache:PoweredEnemyBullet]getBulletHitDamageWithinArea:area];
damage += [[self getBulletCache:BOSSBullet] getBulletHitDamageWithinArea:area];
}
return damage;
} //isBulletHitArea
- (void)dealloc{
[bulletCacheMapData removeAllObjects];
[bulletCacheMapData release];
[sharedBulletCache release];
sharedBulletCache = nil;
[super dealloc];
} //dealloc
@end
我们定义了一个字典,用来存放不同的子弹Cache。addBulletCacheWithType方法用来添加某种子弹Cache到字典中,getBullet方法用来返回某种类型的bullet,getBulletCache方法用来获取CCSpriteBatchNode对象,这个方法用来将CCSpriteBatchNode返回到CCLayer中,作为CCLayer的child添加,不然bullet就不能正常渲染了。初始化方法初始化了两种BulletCache,后面添加完敌人的子弹之后,我们会修改这个初始化方法,将敌人的子弹也添加到Cache中。getBulletHitDamageWithinArea利用BulletCache提供的方法,计算某个区域内各种子弹的总伤害,这里加入了一个BOOL参数,用来判断是玩家的子弹还是敌人的子弹,我们不希望敌人的子弹打到自己,所以进行一下判断是有必要的。
好了,准备工作已经就绪了,下面我们就修改Entity的三个射击方法,代码如下:
-(void)shootBulletAtPosition:(CGPoint) position
bulletType:(BulletTypes) bT{
BulletCacheManager* bullets = [BulletCacheManager sharedBulletCacheManager];
Bullet* bullet = [bullets getBullet:bT];
[bullet initByPos:position];
} //shootBulletAtPosition
-(void)shootBulletAtPosition:(CGPoint) position
atTarget:(CGPoint) target
bulletType:(BulletTypes) bT{
BulletCacheManager* bullets = [BulletCacheManager sharedBulletCacheManager];
Bullet* bullet = [bullets getBullet:bT];
[bullet initByPos:position andTarget:target];
} //shootBulletAtPosition
-(void)shootBulletAtPosition:(CGPoint) position
atAngle:(float) angle
bulletType:(BulletTypes) bT{
BulletCacheManager* bullets = [BulletCacheManager sharedBulletCacheManager];
Bullet* bullet = [bullets getBullet:bT];
[bullet initByPos:position andAngle:angle];
} //shootBulletAtPosition
不多做解释,就是利用Bullet自己的三种构造方法创建子弹就好了。
好了,貌似大功告成了,运行一下,点击发射子弹按钮,什么也没有对么?这就对了,我们之前说过,必须要把CCSpriteBatchNode添加到当前层,里面的纹理才能被正确渲染,所以我们在GameLayer的initCaches方法最后加入下面的代码:
BulletCacheManager*bulletCacheManager = [BulletCacheManager sharedBulletCacheManager];
[self addChild:bulletCacheManager z:-1 tag:BulletCacheManagerTag];
for (BulletCache* bulletCache in [[bulletCacheManager bulletCacheMap]objectEnumerator]) {
[self addChild:bulletCache.bullets];
}
再运行一下,这回子弹能够正常发射了,而且比较流畅,效果如下:
这一篇教程就到这儿了,下一篇我们继续添加各种“可怕”的敌人~~