Cocos2d开发系列.docx
- 文档编号:10459481
- 上传时间:2023-05-25
- 格式:DOCX
- 页数:31
- 大小:32.26KB
Cocos2d开发系列.docx
《Cocos2d开发系列.docx》由会员分享,可在线阅读,更多相关《Cocos2d开发系列.docx(31页珍藏版)》请在冰点文库上搜索。
Cocos2d开发系列
Cocos2d开发系列
LearnIPhoneandiPadCocos2dGameDelevopment》第8章
这种类型的游戏(shoot’emup游戏)最重要的是什么?
射击的目标和需要躲避的子弹。
本章,将为游戏添加一些敌人以及一个大boss。
敌人和玩家将使用新的BulletCache类射击不同的子弹,这些子弹来自同一个pool。
这个缓冲类会重用无效的子弹,以避免重复的内存分配和释放动作。
同样,敌人会使用EnemyCache类,因为待会屏幕上会出现成堆的敌人。
显然玩家可以向敌人射击。
我会介绍基于组件编程的概念,用一种模板化的方式扩展游戏角色。
除了shooting组件和moving组件,我们还会为boss老怪创建healthbar组件(生命值,俗称“血槽”)。
毕竟,老怪不应该是一下就能pk掉的,其生命值总是被一点点减少直至彻底干掉它。
一、添加BulletCache类
该类在是“一站式”的,可以一次性生成许多子弹。
原来这些代码是放在GameScene类中,但这(指生成子弹)显然不该由GameScene来管。
下面显示BulletCache的头文件,它包括了CCSpriteBatchNode对象和无效子弹计数器nextInactiveBullet:
#import"cocos2d.h"
@interfaceBulletCache:
CCNode{
CCSpriteBatchNode*batch;
intnextInactiveBullet;
}
-(void)shootBulletAt:
(CGPoint)startPositionvelocity:
(CGPoint)velocity
frameName:
(NSString*)frameName;
@end
为了把代码重构到GameScene类之外,我需要把initialization方法和射击子弹的方法移到BulletCache类(代码见后)。
接着,我决定使用一个CCSpriteBatchNode变量,以免在每次需要这个对象时就得调用一次[CCNodeCCSpriteBatchNode]方法。
这会带来细微的性能优化。
由于我会在类GameScene中加入BulletCache对象,因此很容易就能把spritebatchnode传给BulletCache。
注意,新的BulletCache有一个问题,增加了scene的层次——一个额外的CCNode。
如果你担心这点,你也可以把spritebatchnode放在GameScene类中,用一个方法从BulletCahce获取这个spritebatchnode。
但是,额外的函数调用开销有可能会使性能得以下降。
如果你怀疑是不是真的对性能由影响,那就让你的代码可读性更好些。
当有必要进行性能优化的时候再重构你的代码。
#import"BulletCache.h"
#import"Bullet.h"
@implementationBulletCache
-(id)init{
if((self=[superinit])){
//从当前贴图集中获得角色帧
CCSpriteFrame*bulletFrame=[[CCSpriteFrameCachesharedSpriteFrameCache]
spriteFrameByName:
@"bullet.png"];
//使用角色帧的贴图构建CCSpriteBatchNode
batch=[CCSpriteBatchNodebatchNodeWithTexture:
bulletFrame.texture];
[selfaddChild:
batch];
//创建子弹并加到batch
for(inti=0;i<200;i++){
Bullet*bullet=[Bulletbullet];
bullet.visible=NO;
[batchaddChild:
bullet];
}
returnself;
}}
-(void)shootBulletAt:
(CGPoint)startPositionvelocity:
(CGPoint)velocityframeName:
(NSString*)frameName{
CCArray*bullets=[batchchildren];
CCNode*node=[bulletsobjectAtIndex:
nextInactiveBullet];
NSAssert([nodeisKindOfClass:
[Bulletclass]],@"notaBullet!
");
Bullet*bullet=(Bullet*)node;
[bulletshootBulletAt:
startPositionvelocity:
velocityframeName:
frameName];
nextInactiveBullet++;
if(nextInactiveBullet>=[bulletscount]){
nextInactiveBullet=0;
}
}
@end
shootBulletAt方法已经完全变了。
它有3个参数:
startPosition,velocity和frameName——取代Ship类指针。
然后这些参数被传递给Bullet类的shootBulletAt方法,这个方法现在已经变为:
-(void)shootBulletAt:
(CGPoint)startPositionvelocity:
(CGPoint)velframeName:
(NSString*)frameName{
self.velocity=vel;
self.position=startPosition;
self.visible=YES;
//改变子弹的贴图,设置一个不同的角色帧去显示
CCSpriteFrame*frame=[[CCSpriteFrameCachesharedSpriteFrameCache]spriteFrameByName:
frameName];
[selfsetDisplayFrame:
frame];
[selfscheduleUpdate];
}
velocity和position被直接赋值给bullet。
这意味着调用shootBulletAt方法的代码必需自己决定子弹的位置、方向和速度。
这出于这样的考虑:
子弹射击的动作会适应更多的变化,包括可以改变子弹的角色帧(用setDisplayFrame方法)。
因为子弹使用的是相同的贴图集、相同的贴图,所以需要通过设置相应的贴图帧来改变子弹的显示。
实际上,渲染贴图的不同部分很轻松,并不会带来额外的开销。
在编辑Bullet类时,我还修正了一个边界问题——只有子弹移出屏幕右边时,才会设为不可见并被放会重用列表(其实这是一个bug)。
通过在update方法中使用CGRectIntersectsRect检查子弹的边框和屏幕矩形,任何完全移出屏幕的子弹都会被标记为重用:
//子弹离开屏幕后,设为不可见
if(CGRectIntersectsRect([selfboundingBox],screenRect)==NO){
……
}
screenRect变量出于方便和性能的原因,被存储为static变量,因此它能被其他类访问,并不需要每次使用的时候创建。
static变量在类实现文件中声明并有效,比如screenRect。
它们就像类的全局变量,任何类实例都可以读取和修改。
成员变量则不同,它们只存在于每个实例对象中。
因为屏幕尺寸在游戏期间永远不会变,所有的子弹都需要用到它,把它存储为所有实例的static变量显然是行得通的。
第一个实例负责给screeenRect赋值。
CGRectIsEmpty方法负责检查screenRect变量是否未初始化——因为是static变量,只需要初始化一次就行了。
staticCGRectscreenRect;
......
//确保只初始化一次
if(CGRectIsEmpty(screenRect)){
CGSizescreenSize=[[CCDirectorsharedDirector]winSize];
screenRect=CGRectMake(0,0,screenSize.width,screenSize.height);
}
接下来,移除GameScene类中原有的用于射击子弹的代码。
此外,需要用初始化BulletCache来替换初始化CCSpriteBatchNode(在GameScene的init方法中):
BulletCache*bulletCache=[BulletCachenode];
[selfaddChild:
bulletCachez:
1tag:
GameSceneNodeTagBulletCache];
还需要为bulletCache添加一个访问方法以便其他类通过GameScene访问BulletCache实例:
-(BulletCache*)bulletCache{
CCNode*node=[selfgetChildByTag:
GameSceneNodeTagBulletCache];NSAssert([nodeisKindOfClass:
[BulletCacheclass]],@"notaBulletCache");return(BulletCache*)node;
}
InputLayer现在可以用BulletCache发射子弹了。
子弹的位置、速度和所用的角色帧这些属性,应当在InputLayer的update方法里传递给射击方法:
if(fireButton.active&&totalTime>nextShotTime){
nextShotTime=totalTime+0.5f;
GameScene*game=[GameScenesharedGameScene];
Ship*ship=[gamedefaultShip];
BulletCache*bulletCache=[gamebulletCache];
//射击前设置position,velocityh和spriteframe
CGPointshotPos=CGPointMake(ship.position.x+[shipcontentSize].width*0.5f,ship.position.y);
floatspread=(CCRANDOM_0_1()-0.5f)*0.5f;
CGPointvelocity=CGPointMake(1,spread);
[bulletCacheshootBulletAt:
shotPosvelocity:
velocityframeName:
@"bullet.png"];
}
重构后的射击过程添加了一些非常必要的灵活性。
你可以设想一下,敌人现在可以使用同样的代码发射它们自己的子弹了。
二、敌人
此刻,对于敌人我们仅有一个模糊的概念,它们是干什么的?
它们的行为是什么?
对于敌人,最重要的是——你永远不知道他们该干什么。
就游戏而言,这意味着一切都要从头开始,要策划出你想让敌人做的事情,从而分析需要编写的代码。
与真实世界不同,你完全控制着你的敌人们。
是不是觉得自己很伟大?
但在你或者其他人感到好笑之前,你需要为统治世界想出一个计划。
我创建了3种不同类型的敌人的图片。
这里,我只知道其中一个应该是Boss。
看一眼下面的图片,然后想象一下这些敌人分别能干些什么:
在写代码之前,先了解一下这些敌人有哪些行为是共性的,这样有些代码只用编写一次。
代码复用是最重要的编码规范。
我们先来看看敌人们都有哪些共性:
¥发射子弹
¥何时何地发射子弹的判断逻辑
¥能被玩家的子弹击中
¥不能被其他敌人的子弹击中
¥能被多次击中(有生命值)
¥有固定的行为和移动方式
¥死亡时显示特定的行为或动画
¥从屏幕以外进入屏幕后将会显示
¥当移出屏幕后将不再显示
你可能注意到,上面有些特性也符合玩家飞船。
飞船也可以射击子弹,它也可能经受多次射击;当它被摧毁时也应该呈现某个行为或动画,它给人的感觉类似一个特殊的敌人。
扫描上述列表,会有3种实现方式。
可以创建一个类,把飞船、敌人、Boss都包含在其中。
代码将是有选择地执行部分代码,这取决于敌人的类型。
例如,射击代码可能为不同的类型提供不同的分支。
对于对象有限的游戏,这是不错的办法——但它无法面对大规模的对象。
随着游戏中加入越来越多地对象,你的游戏代码必将变得肥大臃肿。
对这个类的任何部分进行修改,都会潜在地对敌人或者飞船的行为带来不希望的影响。
用一个变量——敌人类型来决定代码执行路径是一种古老的C编程方式,不符合O-C的面向对象特性。
这种方式至今仍然非常有用,但一定要慎用。
第二种方式,是创建一个类层次。
用一个Entity类作为基类,从它派生出一个飞船类、2个怪物类、1个Boss类。
很多程序员常这样干,对于游戏对象不多的情况这种方式也非常好用。
但本质上,这和第一种方式没什么不同。
基类封装了子类要用到的一些通用代码,但不是全部代码。
当Entity类中的代码开始基于某个子类的类型执行某个分支时,情况变得糟糕——跟第一种方法一样了。
如果小心一点,你应该确保把针对某种敌人的代码放在某个子类里,但在修改的时候很容易会把很多改动放到Entity类里。
第3种方式,是使用组件编程。
这意味着不同的代码路径从Entity类层次结构中分离出来,这部分代码仅仅加到所需的子类中。
比如一个“血槽”组件。
基于组件的编程可以单独写成一本书,对于射击游戏这类项目而言,这显得有些杀鸡用牛刀了,因此我会混合后面两种方式一起使用,这里只是给出一个概念:
如何组合游戏对象而不是各自为政,以及这样做的好处。
我想说明的是,不存在最好的编码方式。
选择某种方式完全是主观的,取决于个人经验和偏好。
如果你愿意随着对手上开发的游戏的逐渐理解,不断重构你的代码库,能运行的代码比干净的代码更可取。
经验让你不经过计划就能做出这些决定,让你能更快地创建更多复杂游戏。
要想达到这个目的,从完成一个小游戏开始,然后慢慢地挑战自己的极限。
这是个需要学习的过程,很不幸的,在这个过程中你的学习兴趣也很容易被好高骛远消灭掉。
为什么每个老练的游戏编程人员会告诉新人,从简单入手,去重写经典的电玩游戏比如俄罗斯方块、帕克人、小行星。
三、Entity类
Entity类是继承自CCSprite,只包含了Ship类中的setPosition方法定义,以使所有的Entity实例始终在屏幕内移动。
我只对代码做了一小点改动(其实就是如下面代码所示的if语句,原来的代码是没有if语句的),屏幕外的对象可以移动到屏幕内,但一旦进入屏幕后,它们不能再离开屏幕区域。
在这个射击类游戏中,敌人不会从你身边走开,而是站在屏幕中间为了演示一下EnemyCache,进行简单的介绍。
屏幕区域检查只是简单检查一下sprite的边框是否完全被屏幕边框所包含,如果是的话,代码将让sprite始终保持在屏幕边框内:
-(void){
}
setPosition:
(CGPoint)pos
//如果当前位置在屏幕外,则不需要让位置调整到屏幕内
//这会允许对象从屏幕外部移动到屏幕内部
if(CGRectContainsRect([GameScenescreenRect],[selfboundingBox])){
...
[supersetPosition:
pos];
}
ShipEntity类取代了Ship类。
由于Entity类已经包含了setPosition方法,ShipEntity类只剩下了initWithShipImage方法。
该方法的代码没有改变。
四、EnemyEntity类
我们需要继续深入到EnemyEntity类,首先是头文件:
#import
#import"Entity.h"
typedefenum{
EnemyTypeBreadman=0,
EnemyTypeSnake,
EnemyTypeBoss,
EnemyType_MAX,
}EnemyTypes;
@interfaceEnemyEntity:
Entity{
EnemyTypestype;
}
+(id)enemyWithType:
(EnemyTypes)enemyType;
+(int)getSpawnFrequencyForEnemyType:
(EnemyTypes)enemyType;
-(void)spawn;
@end
没有什么特别的。
EnemyTypes枚举用于3种不同的敌人类型,EnemyType_MAX用于在遍历时标志结束。
EnemyEntity类使用了一个EnemyTypes变量存储类型,因此我可以用switch命令基于敌人的类型构建分支语句。
EnemyEntity的实现包含许多代码,我会把它分成几个主题,并只显示相关的代码。
首先是initWithType方法:
-(id)initWithType:
(EnemyTypes)enemyType
{
type=enemyType;
NSString*frameName;
NSString*bulletFrameName;
intshootFrequency=300;
switch(type)
{
caseEnemyTypeBreadman:
frameName=@"monster-a.png";
bulletFrameName=@"candystick.png";
break;
caseEnemyTypeSnake:
frameName=@"monster-b.png";
bulletFrameName=@"redcross.png";
shootFrequency=200;
break;
caseEnemyTypeBoss:
frameName=@"monster-c.png";
bulletFrameName=@"blackhole.png";
shootFrequency=100;
break;
default:
[NSExceptionexceptionWithName:
@"EnemyEntityException"reason:
@"unhandledenemytype"userInfo:
nil];
}
if((self=[superinitWithSpriteFrameName:
frameName]))
{
//Createthegamelogiccomponents
[selfaddChild:
[StandardMoveComponentnode]];
StandardShootComponent*shootComponent=[StandardShootComponentnode];
shootComponent.shootFrequency=shootFrequency;
shootComponent.bulletFrameName=bulletFrameName;
[selfaddChild:
shootComponent];
//enemiesstartinvisible
self.visible=NO;
[selfinitSpawnFrequency];
}
returnself;
}
方法一开始是变量赋值,根据敌人的类型,使用switch语句为每种类型提供默认值:
敌人的角色帧名以及子弹的角色帧名。
switch的default分支抛出异常,因为其他类型在Enemytypes枚举中未定义。
这样,如果你定义了一种新的敌人类型,但是如果它不会动,或者发射出了错误的子弹,那么你会得到一个错误警告:
哈,你忘记修改某些东西了!
最后别忘了调用[superinit…]方法,否则super无法正确初始化并导致一个奇怪的错误然后崩溃。
接下来创建了一个组件,并把它加到EnemyEntity中。
后面我会访问这个组件,在此你只需要知道StandardMoveComponent能让敌人移动并射击。
把注意力放到initSpawnFrequency方法。
-(void)initSpawnFrequency
{
//initializehowfrequenttheenemieswillspawn
if(spawnFrequency==nil)
{
spawnFrequency=[[CCArrayalloc]initWithCapacity:
EnemyType_MAX];
[spawnFrequencyinsertObject:
[NSNumbernumberWithInt:
80]atIndex:
EnemyTypeBreadman];
[spawnFrequencyinsertObject:
[NSNumbernumberWithInt:
260]atIndex:
EnemyTypeSnake];
[spawnFrequencyinsertObject:
[NSNumbernumberWithInt:
1500]atIndex:
EnemyTypeBoss];
//spawnoneenemyimmediately
[selfspawn];
}
}
+(int)getSpawnFrequencyForEnemyType:
(EnemyTypes)enemyType
{
NSAssert(enemyType NSNumber*number=[spawnFrequencyobjectAtIndex: enemyType]; return[numberintValue]; } -(void)dealloc { [spawnFrequencyrelease]; spawnFrequency=nil; [superdealloc]; } 我们把每种类型的敌人的出场频率记录在静态数组spawnFrequency里。 第一个EnemyEntity实例负责初始化CCArray数组。 CCArray不能存储原始数据类型比如整型,因此使用了NSNumber类。 使用insertObject方法而不用addObject方法是为了保证对象加入时的顺序,同时别人看到这个枚举值也映射了对应的敌人类型。 dealloc方法释放了CCArray对象,并将其设为nil,这点非常重要。 作为静态变量,第一个EnemyEntity对象在运行其dealloc方法时会释放spawnFrequency的内存,如果spawnFrequency不被设为nil,下一个EnemyEntity对象的dealloc方法将视图再次释放,这会“过度释放”s
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- Cocos2d 开发 系列