(译)cocos2d精灵教程:第一部分

原文链接地址:http://www.iphonegametutorials.com/2010/09/10/cocos2d-sprite-tutorial/

教程截图:

  前言:CocoaChina上有位网友问我能不能翻译一些比较高级的文章。首先,非常感谢这位网友的建议。之前翻译的3篇菜单教程,相比于前面TinyWings来说,确实显得有点小儿科了。但是,需要说明的是,那三篇菜单教程是后面教程的基础。在iphonegametutorials这个网站上,作者会引用到前面的文章。所以今天,我还是会翻译三篇“小儿科”的文章。不过希望大家还是耐着性子看一遍,权当温故知新。如果大家觉得哪些主题比较感兴趣,麻烦把E文链接发给我,我可以翻译。同时,我希望本博客能给游戏开发人员提供一些帮助,我希望有兴趣的开发人员能够加入到翻译的队伍中来,我需要你们的帮助,毕竟我一个人的时间和精力有限。当然,我们也可以自己写游戏开发教程:)。比如知易写的就很不错,通俗易懂。如果有意向一起翻译或者愿意写游戏教程的朋友,请加我qq:254041321。谢谢大家一直关注我!

  当然,如果有自己的博客,并且写了教程的,也可以把链接发给我,我转载并注明版权和出处。总之,我希望这个博客是我们共同的游戏开发博客。上面会有很多的资源和分享。希望得到你们的支持和参与,再次表示感谢!

----------------------------------------------------------------------------------------------------------------------

  朋友们,欢迎回来!今天,我们将要征服cocos2d里面的精灵。这个过程并不会像你想像中那么难,接下来的教程,我就会证明给你看。首先,我们有N种方法在屏幕上显示一张图片。。。其实,我们在《coco2d菜单教程:第三部分》就已经知道一种显示图片的方式了。

 

  那么,我们今天将学习哪些内容呢?我们将学习有关 “CCSprite”, “CCSpriteSheets”, “CCSpriteFrame”,以及“Texture2d” 和 “CCTextureCache”的一切!在这篇教程的最后,我们将有一条龙在一个简单的背景地形上面飞,路径由用户的手指滑动touch决定。很酷吧?

  这里有本教程的完整源代码

(。。。这里省略掉了一大段内容,讲的是0.99.5对api接口的变动,现在我们都用1.0了,这些内容我也不翻译了。。。)

    因此,在这个教程中,我们还是学习一些基础知识--不过没有菜单啦,我们主要关心的是精灵(sprite)。先看一看整个教程最后的产品是什么样子吧!如下图所示:

 

  是否有点激动了呢?我敢打赌你现在很激动。。。不管怎么说,直接切入主题吧。。。

  首先,我们使用上个教程的 SceneManager.h/.m ,这里我们只创建“PlayLayer”类。

  我们首先要做的就是把“dragon.png”图片拖到Resources文件夹下去,它是一张750×560的图片,我把它缩小显示在下图。你可以从这里下载完整大小的图片资源。

 

  那里面还有一张地形背景图,它的大小为480×320,我也把它缩小显示出来了:

 

  像之前的菜单教程一样,把这里图片添加进resources分组下面去--你按住ctrl-click在“Resources”文件夹里面挑选你想要添加进工程的图片,记住,如果图片不是放在Reources文件夹下,那么最好选中 “copy to directory”复选框。

  好了,一旦你添加进去以后,它看起来的结构如下图所示:

 

  我们的工程将包含两个类(delegate类除外): SceneManager 和 PlayLayer。我们已经知道SceneManger是如何工作的了,这里就不再啰嗦了。(可以参考《cocos2d菜单教程:第一部分》),但是,下面是我移除掉一些菜单选项后的SceneManager类。

SceneManager.h

#import 

#import
"PlayLayer.h"

@interface SceneManager : NSObject {
}

+(void) goPlay;
@end

SceneManager.m

#import "SceneManager.h"

@interface SceneManager ()
+(void) go: (CCLayer *) layer;
+(CCScene *) wrap: (CCLayer *) layer;
@end

@implementation SceneManager

+(void) goPlay{
CCLayer
*layer = [PlayLayer node];
[SceneManager go: layer];
}

+(void) go: (CCLayer *) layer{
CCDirector
*director = [CCDirector sharedDirector];
CCScene
*newScene = [SceneManager wrap:layer];

if ([director runningScene]) {
[director replaceScene:newScene];
}
else {
[director runWithScene:newScene];
}
}

+(CCScene *) wrap: (CCLayer *) layer{
CCScene
*newScene = [CCScene node];
[newScene addChild: layer];
return newScene;
}
@end

 

  一定要记得,把delegate类里面的CCDirector replaceScene调用,改成 “[SceneManager goPlay];”调用。

  好,如果你们都看了菜单教程的话,那么看到这里,你们可能会觉得烦了。所以,来点新鲜的吧:

PlayLayer.h

#import "cocos2d.h"

#import
"SceneManager.h"

@interface PlayLayer : CCLayer {
NSMutableArray
*_flyActionArray;

CCSprite
*_dragon;
CCAction
*_flyAction;
CCAction
*_moveAction;
BOOL _moving;
}

@property (nonatomic, retain) NSMutableArray
*flyActionArray;

@property (nonatomic, retain) CCSprite
*dragon;
@property (nonatomic, retain) CCAction
*flyAction;
@property (nonatomic, retain) CCAction
*moveAction;
@end

 

  那么,这里做了些什么事呢?我们创建了三个实例变量:_dragon, _flyAction and _moveAction.从名字差不多也可以看出来它们到底是干什么用的,_dragon是我们的主角精灵,_flyAnimmation负责处理dragon的煽动翅膀的动画,而_moveAction负责处理从一点移动到另一个点。

  因此,CCSprite非常重要,但是,你可能会问你自己,我到底是应该继承CCSprite,还是包含一个CCSprite实例呢?我这里不使用一个Dragon类来继承CCSprite的原因是你可以从文件中加载一张图片来初始化它。这里有一个非常著名的问题:“Is-a”还是“has-a”?好吧,我的喜好是派生至CCNode,然后把所有的CCSprite当作它的属性。因为,我相信这样做会给你最大的灵性性。我承认,如果从CCSprite继承的话,刚开始会有许多方便之处,比如可以直接添加到BatchNode等。但是,我还是坚信,把CCSprite当作一个属性的话,你可以在以后的编程中获得巨大的好处。

  小提示:属性是objc提供给我们的一种方便地生成get/set方法的一种机制,同时还有其它一些好处,比如可以协助内存管理。

For example:

@interface HeroPlayer : NSObject {
NSString
* weapon;
NSString
* armor;
}

- (NSString*) weapon;
- (NSString*) armor;

- (void) setWeapon: (NSString*)input;
- (void) setArmor: (NSString*)input;

 

   @property是objc声明属性的关键字,括号里面的retain指明set方法时,传入的参数会调用retain,使其引用计数加1.(译者:这样的话,你可以用self.xxx = [CCNode node];因为,cocos2d里面大量使用autorelease对象,这种对象出了当前mainLoop的话就会被release掉。所以,经常需要retain。而使用self.xxx = [CCNode node];的话,如果xxx属性里面的括号声明成了retain,那么就不用再retain了。简言之,self.xxx = [CCNode node];  和 xxx = [[CCNode node] retain];等价。但是,我们很少会在cocos2d里面retain什么东西,因为,我们基本上都会调用addChild方法,而这个方法会把参数自动retain一次,所以产生的autorelease对象才不会被释放掉。)

@implementation HeroPlayer

@synthesize weapon;
@synthesize armor;
...
@end

 

  @synthesize会自动为我们生成getter和setter。因此,我们只需要为这个类实现dealloc方法就可以了。

  访问器(getter/setter)只有当它们不存在的时候才生成,所以可以放心使用 @synthesize来声明属性,之后,你再可以按需要定制相应的getter和setter。编译器会判断哪个方法没有,然后自已生成之。

  好了,刚刚讲了一些题外话。。。接下来,让我们看看PlayLayer的实现:

PlayLayer.m

#import "PlayLayer.h"

@implementation PlayLayer

@synthesize flyActionArray
= _flyActionArray;

@synthesize dragon
= _dragon;
@synthesize moveAction
= _moveAction;
@synthesize flyAction
= _flyAction;

enum {
kTagSpriteSheet
=1,
};

-(id) init{
self
= [super init];
if (!self) {
return nil;
}

CCSprite
*background = [CCSprite spriteWithFile:@"Terrain.png"];
background.position
= ccp(160, 240);
[self addChild:background];

CCTexture2D
*texture = [[CCTextureCache sharedTextureCache] addImage:@"dragon.png"];

CCSpriteSheet
*sheet = [CCSpriteSheet spriteSheetWithTexture:texture capacity:10];
[self addChild:sheet z:
0 tag:kTagSpriteSheet];

CGSize s
= [[CCDirector sharedDirector] winSize];

_flyActionArray
= [[NSMutableArray alloc] init];
NSMutableArray
*animFrames = [NSMutableArray array];

for (int i =0; i <8; i++) {

[animFrames removeAllObjects];

for (int j =0; j <10; j++) {
CCSpriteFrame
*frame = [CCSpriteFrame frameWithTexture:texture rect:CGRectMake(j*75, i*70, 75, 70) offset:CGPointZero];
[animFrames addObject:frame];
}
CCAnimation
*animation = [CCAnimation animationWithName:@"fly" delay:0.1f frames:animFrames];
CCAnimate
*animate = [CCAnimate actionWithAnimation:animation restoreOriginalFrame:NO];
CCSequence
*seq = [CCSequence actions: animate,
nil];

self.flyAction
= [CCRepeatForever actionWithAction: seq ];
[_flyActionArray addObject:self.flyAction];
}

CCSpriteFrame
*frame1 = [CCSpriteFrame frameWithTexture:texture rect:CGRectMake(0, 0, 75, 70) offset:CGPointZero];

self.dragon
= [CCSprite spriteWithSpriteFrame:frame1];
_dragon.position
= ccp( s.width/2-80, s.height/2);

[sheet addChild:_dragon];

self.flyAction
= [_flyActionArray objectAtIndex:0];
[_dragon runAction:_flyAction];

self.isTouchEnabled
= YES;

return self;
}

- (void) dealloc
{
[[CCSpriteFrameCache sharedSpriteFrameCache] removeUnusedSpriteFrames];
[self.flyActionArray removeAllObjects];

self.dragon
= nil;
self.flyAction
= nil;
self.moveAction
= nil;
[super dealloc];
}

@end

 

  ok,上面的代码看起来比较长,不过没关系,让我们分别解释下:

  首先是加载背景图片

CCSprite *background = [CCSprite spriteWithFile:@"Terrain.png"];
background.position
= ccp(160, 240);
[self addChild:background];

 

  背景图片加载后放置在屏幕的中心(默认情况下,图片的anchorPoint是图片的中心,你可以使用anchorPoint属性来改变它--举个例子:把背景的anchorPoint从中心点

background.anchorPoint = ccp(0.5,0.5);

  改成图片的左下角:

background.anchorPoint = ccp(0.0,0.0);

  接下来:

CCTexture2D *texture = [[CCTextureCache sharedTextureCache] addImage:@"dragon.png"];

CCSpriteSheet
*sheet = [CCSpriteSheet spriteSheetWithTexture:texture capacity:10];
[self addChild:sheet z:
0 tag:kTagSpriteSheet];

 

  CCTextureCache和CCTexture2D是n种方式中的一种,可以把一张图片加载到CCSprite中去。我是认真的,真的有n种方法可以做这个事情。。。现在,我不想让你头晕,所以先不列举其它方法了。。。让我们先看看这段代码都做了些什么吧。首先,加载一张dragon.png图片到texture对象中。

  因此,接下来,好多人都对CCSpriteSheet的使用感到迷惑不解(译者:大家注意,现在没有这个类了,改成CCSpriteBatchNode来代替了,但是思想还是一样的)。一个CCSpriteSheet是一种效率比较高的渲染精灵的方式。比如,你把CCSprite加到CCLayer中,那么sprite的draw函数在每一帧调用时都会执行7个opengl 调用来完成sprite的渲染。一个精灵的时候当然没问题,但是,当你在屏幕上有200个精灵的时候,那么就会有200×7次opengl调用。。。而CCSpriteSheet(或者CCSpriteBatchNode)可以“批处理”它的孩子精灵的draw调用。这意味着,当把200个精灵加到Spritesheet中去的时候,只要使用7个opengl调用就可以完成200个孩子的渲染了。在本例中,我们看到dragon类也有许多精灵,所以我们要使用spritesheet。

更新:关于CCSpriteBatchNode的误解,请参看泰然论坛这个帖子

  Riq在cocos2d for iphone里面提到:  

  1.通过使用纹理集(texture atlas),你可以加快游戏的加载时间,同时减少耗费的内存资源。

  2.通过使用CCSpriteSheet(CCSpriteBatchNode),你可以提供渲染的性能,(具体查看Performance Tests)

  3.通过使用CCTextureCache和CCSpriteSheet,那么你可以同时获得加载时间的性能和渲染的性能提升。

  当然,现在只有一只龙,可能看不出明显的效果提升。但是,这是最佳实践,你最好一开始就按照这种方式去做,它会为你以后省去很多麻烦事。

_flyActionArray = [[NSMutableArray alloc] init];
NSMutableArray
*animFrames = [NSMutableArray array];

for (int i =0; i <8; i++) {

[animFrames removeAllObjects];

for (int j =0; j <10; j++) {
CCSpriteFrame
*frame = [CCSpriteFrame frameWithTexture:texture rect:CGRectMake(j*75, i*70, 75, 70) offset:CGPointZero];
[animFrames addObject:frame];
}
CCAnimation
*animation = [CCAnimation animationWithName:@"fly" delay:0.1f frames:animFrames];
CCAnimate
*animate = [CCAnimate actionWithAnimation:animation restoreOriginalFrame:NO];
CCSequence
*seq = [CCSequence actions: animate,
nil];

self.flyAction
= [CCRepeatForever actionWithAction: seq ];
[_flyActionArray addObject:self.flyAction];
}

 

  上面这个部分似乎做了很多事情--但是,实际上很简单。我们使用CCSpriteFrame来创建精灵动画帧,一个CCSpriteFrame就是从texture里面抠出来的一块小图片区域,需要指定矩形区域。它并不包含图片本身,而是更像一帧图像。每一次,动画运行的时候,就会运行一系列的CCSpriteFrames,而每个CCSpriteFrames指向texture里的一小块图片。

  因为我们的dragon每个动画都有10张图片,总共有8个方向飞行的动画(对应东南西北8个方向的动画)。我们创建了80个帧,然后把每一个方向飞行的动画都存到NSMutableArray里面。然后,我们把所有的flyAction再添加到_flyActionArray里面去。

CCSpriteFrame *frame1 = [CCSpriteFrame frameWithTexture:texture rect:CGRectMake(0, 0, 75, 70) offset:CGPointZero];

self.dragon
= [CCSprite spriteWithSpriteFrame:frame1];
_dragon.position
= ccp( s.width/2-80, s.height/2);

[sheet addChild:_dragon];

self.flyAction
= [_flyActionArray objectAtIndex:0];
[_dragon runAction:_flyAction];

 

  因为dragon刚开始时需要有一张图片作为初始状态,所以,我们从animation里面取出第一帧(位置在0,0,宽度是75,高度是70)。我们把dragon精灵初始位置设在屏幕中间偏左一点,然后把它加到spritesheet中,目的是为了获得更好的性能提升。

  然后,我们开始运行第一个动画。。。现在,我们可以飞啦!

  下篇教程见!

-------------------------------------------------------------------------------------------------------------------------

后记:这里使用的方法,说实话,真的过时了。:)不过我们了解了也是有好处的。之前翻译的ray的教程里面,都是使用texturePacker生成pvr.ccz和plist文件来处理的。获得动画帧也很简单,直接spriteFrameByName就可以了。

  最后,对于教程开头的“大胆的建议”,如果大家没什么兴趣的话,就看看,然后一笑而过吧~

 

著作权声明:本文由http://www.cnblogs.com/andyque翻译,欢迎转载分享。请尊重作者劳动,转载时保留该声明和作者博客链接,谢谢!

posted on 2011-07-20 13:04  子龙山人  阅读(16289)  评论(8编辑  收藏  举报