(译)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翻译,欢迎转载分享。请尊重作者劳动,转载时保留该声明和作者博客链接,谢谢!