Cocos2Dx之动画-欧阳左至

动画实际上是眼睛造成的错觉。一般情况下,如果每秒连续放映24张以上图片,人眼就看不出有停顿。如果帧速达到了60,甚至更高,那么基本上没人可以感觉到停顿了。动画就是利用这一特性,在每个连续的帧里面,让物体做一些小的变化,从而让人感觉物体自己在动一样。

就像下面的马儿,8张图片以1/60秒的速度播放,就是一只奔跑中的马儿。

动画相比与动作,可以完成更加复杂精细的动态效果,这是动作做不到的。动画需要做的就是在渲染每一帧的时候,连续地显示另外一张图片即可。但是图片是以文件形式存在的,我们要显示它,就需要把它读入内存,然后传给显存,最后再在屏幕上显示出来。图片文件一般都很大,特别是没有压缩过的文件。帧的绘制又必须在特定的时间内完成,如果不能完成,会导致显示错误,比如显示不全,抖动等。因此我们需要减少读文件和大量图片数据的传输。可以采取的办法有:

  • 将动画的每帧纹理都放到一个大的文件当中。显示的时候,指定显示的区域即可。把显示的区域称为帧框。由于减少了文件的读取次数。文件一次读入后,就存在于内存,移动帧框比重新读取就要快很多。

  • 将动画需要的纹理预先读入,并且缓存起来。在绘制帧的时候,再去读取文件,可能还是来不及了。因此,我们可以在使用之前就将纹理全部读入。现在的内存/显存容量已经很大,小游戏使用的纹理完全可以在游戏开始之前就全部载入。

这里先澄清下纹理和图片。纹理指的是一张表示物体表面细节的位图。位图是一种图片,但并不是所有的图片都可以作为纹理的,比如矢量图,或者当前平台不支持的图片格式。纹理的制作,一般不是码农可以兼任的。我们关心的是拿到纹理后怎么创建动画效果。

方式一:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
CCAnimation* animation = CCAnimation::create();
for int  i=1;i<15;i++)
{
     char  szName[100] = {0};
     sprintf (szName,  "Images/horse_%02d.png" , i);
     animation->addSpriteFrameWithFileName(szName);
}
// should last 1.5 seconds. And there are 8 frames.
animation->setDelayPerUnit(1.5f / 8f);
// whether or not it shall restore the original frame when the animation finishes
animation->setRestoreOriginalFrame( true );
CCAnimate* action = CCAnimate::create(animation);
m_grossini->runAction(CCSequence::create(action, action->reverse(), NULL));

方式二:

?
1
2
3
4
5
6
7
8
9
10
11
12
CCTexture2D* texture = CCTextureCache::sharedTextureCache()->addImage(s_horsePath);
float  w = texture->getContentSize().width / numOfHorseFrame;
float  h = texture->getContentSize().height;
CCAnimation* animation = CCAnimation::create();
// should last 1.5 seconds. And there are 8 frames.
animation->setDelayPerUnit(1.5f / 8f);
// whether or not it shall restore the original frame when the animation finishes
animation->setRestoreOriginalFrame( true );
for ( int  i = 0; i < numOfHorseFrame; i ++)
     animation->addFrameWithTexture(texture, CCRectMake(i * w, 0, w, h));
CCAnimate* animate = CCAnimate::create(animation);
horse->runAction(CCRepeatForever::create(animate));

方式一示例通过没帧一个文件创建一个动画。方式二通过在一个纹理上移动帧框创建一个动画,并且启用了缓存。方式二稍微复杂一点,但是从效率上来讲比方式一要高。我们看看方式二都做了些什么:

  • 第一步,先通过CCTextureCache::addImage载入了一个纹理。

  • 第二步,创建了一个动画CCAnimation。设定了动画的每帧执行时间、是否循环使用,然后通过帧框指定了动画每帧显示的纹理区域。显示顺序跟添加的顺序一致。

  • 第三步,创建一个动画动作CCAnimate,然后在精灵上执行它。

后面的内容会依据这三步为主线,分析动画的整个创建的执行过程。在这之前,再看两个需要使用到的类:

  • 框帧(CCSpriteFrame):包含纹理与纹理中的一个矩形区域,表示纹理的一部分。一个精灵显示的内容就可以用框帧表示,同时框帧还是帧动画的基本元素。

  • 动画帧(CCAnimationFrame):由框帧与单位延时组成,可以表示变速动画中的一帧。通常,匀速动画的单位延时为1。

载入并缓存纹理

CCTextureCache是一个纹理缓存。CCTextureCache单例内部用键值对CCDictionary缓存了所有的纹理。CCDictionary的键是纹理的文件地址,指是一个2D纹理对象CCTexture2D。

CCTextureCache::addImage添加一个文件到纹理缓存当中。如果该文件已经添加过,直接返回一个CCTexture2D引用。如果没有添加过,就利用文件名作为键,新建一个键值对。addImage支持的文件格式包括:.png,.bmp, .tiff, .jpeg, .pvr, .gif。图片跟平台的支持有关,有些图片格式可能是平台独有的。不同平台对图片的解码的支持也不均,因此Cocos2Dx为了跨平台图片的处理使用了第三方库。我们现在不必关心图片读取处理的细节,我们关心纹理如何产生的即可。addImage读取文件,生成一个CCImage对象,他是Cocos2Dx里面图片的统一表示。CCImage的图片属性包括:

  • m_nWidth,图片宽度

  • m_nHeight,图片高度

  • m_nBitsPerComponent,像素使用的比特数

  • m_pData,存储图片数据

  • m_bHasAlpha,支持α通道

  • m_bPreMulti,支持预乘α通道 

得到CCImage对象后,利用它创建一个2D纹理对象CCTexture2D。在老版本的OpenGL中,有个限制条件:纹理的大小必须是2的幂,比如268、512、1024。Cocos2Dx的CCTexture2D也有这样的限制条件。但是我们的文件经常不会匹配这样的纹理大小要求,因此纹理大小可能比图片的大小要大一些。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
CCTexture2D * CCTextureCache::addImage( const  char  * path)
{
     CCTexture2D * texture = NULL;
     CCImage* pImage = NULL;
     std::string pathKey = path;
     pathKey = CCFileUtils::sharedFileUtils()->fullPathForFilename(pathKey.c_str());
     if  (pathKey.size() == 0)
     {
         return  NULL;
     }
     texture = (CCTexture2D*)m_pTextures->objectForKey(pathKey.c_str());
     std::string fullpath = pathKey;  // (CCFileUtils::sharedFileUtils()->fullPathFromRelativePath(path));
     if  (! texture)
     {
         std::string lowerCase(pathKey);
         for  (unsigned  int  i = 0; i < lowerCase.length(); ++i)
         {
             lowerCase[i] =  tolower (lowerCase[i]);
         }
         do
         {
             if  (std::string::npos != lowerCase.find( ".pvr" ))
             {
                 texture =  this ->addPVRImage(fullpath.c_str());
             }
             else  if  (std::string::npos != lowerCase.find( ".pkm" ))
             {
                 texture =  this ->addETCImage(fullpath.c_str());
             }
             else
             {
                 CCImage::EImageFormat eImageFormat = CCImage::kFmtUnKnown;
                 if  (std::string::npos != lowerCase.find( ".png" ))
                 {
                     eImageFormat = CCImage::kFmtPng;
                 }
                 else  if  (std::string::npos != lowerCase.find( ".jpg" ) || std::string::npos != lowerCase.find( ".jpeg" ))
                 {
                     eImageFormat = CCImage::kFmtJpg;
                 }
                 else  if  (std::string::npos != lowerCase.find( ".tif" ) || std::string::npos != lowerCase.find( ".tiff" ))
                 {
                     eImageFormat = CCImage::kFmtTiff;
                 }
                 else  if  (std::string::npos != lowerCase.find( ".webp" ))
                 {
                     eImageFormat = CCImage::kFmtWebp;
                 }
                 pImage =  new  CCImage();
                 bool  bRet = pImage->initWithImageFile(fullpath.c_str(), eImageFormat);
                 texture =  new  CCTexture2D();
                
                 if ( texture &&
                     texture->initWithImage(pImage) )
                 {
                     m_pTextures->setObject(texture, pathKey.c_str());
                     texture->release();
                 }
                 else
                 {
                     CCLOG( "cocos2d: Couldn't create texture for file:%s in CCTextureCache" , path);
                 }
             }
         while  (0);
     }
     CC_SAFE_RELEASE(pImage);
     return  texture;
}

将纹理缓存起来后,虽然CCTextureCache::addImage当即返回了纹理对象,但我们后面再使用怎么办呢?CCTextureCache::textureForKey直接根据文件,从缓存从找出并返回缓存的纹理对象。

动画CCAnimation

CCAnimation是用来描述动画的。我们看看描述动画需要哪些信息:

  • m_fTotalDelayUnits,总共有多少个动画帧

  • m_fDelayPerUnit,每个动画帧持续的时间

  • m_fDuration,是m_fTotalDelayUnits与m_fDelayPerUnit之积,整个动画持续的时间

  • m_pFrames,一个CCAnimationFrame数组,表示动画的一帧

  • m_bRestoreOriginalFrame,是否在动画结束是回到第一帧

  • m_uLoops,动画执行次数

?
1
2
3
4
5
6
7
8
9
10
11
12
13
void  CCAnimation::addSpriteFrameWithTexture(CCTexture2D *pobTexture,  const  CCRect& rect)
{
     CCSpriteFrame *pFrame = CCSpriteFrame::createWithTexture(pobTexture, rect);
     addSpriteFrame(pFrame);
}
void  CCAnimation::addSpriteFrame(CCSpriteFrame *pFrame)
{
     CCAnimationFrame *animFrame =  new  CCAnimationFrame();
     animFrame->initWithSpriteFrame(pFrame, 1.0f, NULL);
     m_pFrames->addObject(animFrame);
     animFrame->release();
     m_fTotalDelayUnits++;
}

 CCAnimation::addSpriteFrameWithTexture为每一个帧框创建了一个CCSpriteFrame对象,然后用CCSpriteFrame构造一个CCAnimationFrame添加到CCAnimation的m_pFrames数组当中。

动画动作CCAnimate

有了动画需要使用的数据CCAnimation,后CCAnimate就可以执行动画动作了。

CCAnimate继承自CCActionInterval,是一个持续性动作。持续性动作需要指定动作执行的时间,CCAnimate的执行时间不仅仅是CCAnimation的m_fDuration执行时间,还需要乘上动画循环的次数。上一篇中,我们提到过CCActionInterval的update函数的参数t是一个[0-1]的浮点数,表示归一化的进度。CCAnimate的浮点数组m_pSplitTimes存储了归一化的动画没帧开始时间的进度,这是一种时间换空间的做法。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
bool  CCAnimate::initWithAnimation(CCAnimation *pAnimation)
{
     float  singleDuration = pAnimation->getDuration();
     if  ( CCActionInterval::initWithDuration(singleDuration * pAnimation->getLoops() ) )
     {
         m_nNextFrame = 0;
         setAnimation(pAnimation);
         m_pOrigFrame = NULL;
         m_uExecutedLoops = 0;
         m_pSplitTimes->reserve(pAnimation->getFrames()->count());
         float  accumUnitsOfTime = 0;
         float  newUnitOfTimeValue = singleDuration / pAnimation->getTotalDelayUnits();
         CCArray* pFrames = pAnimation->getFrames();
         CCARRAY_VERIFY_TYPE(pFrames, CCAnimationFrame*);
         CCObject* pObj = NULL;
         CCARRAY_FOREACH(pFrames, pObj)
         {
             CCAnimationFrame* frame = (CCAnimationFrame*)pObj;
             float  value = (accumUnitsOfTime * newUnitOfTimeValue) / singleDuration;
             accumUnitsOfTime += frame->getDelayUnits();
             m_pSplitTimes->push_back(value);
         }
         return  true ;
     }
     return  false ;
}

动作的实际执行是调用CCAnimate::update。在游戏的每一帧渲染过程中,都会调用CCAnimate::update。由于需要考虑循环,稍微增加了一些实现的复杂度。update的参数t是整个动画按循环次数完成的百分比,因此t先乘以循环次数得到已经循环的次数(取整)。t乘过后,还需要修正回来,因此随后去1.0f进行了浮点数余数计算,得到当前循环完成的进度t。然后再拿这个进度t,去m_pSplitTimes找数值比起小的动画帧,然后将动画帧对应的纹理应用到目标精灵上。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
void  CCAnimate::update( float  t)
{
     if ( t < 1.0f ) {
         t *= m_pAnimation->getLoops();
         unsigned  int  loopNumber = (unsigned  int )t;
         if ( loopNumber > m_uExecutedLoops ) {
             m_nNextFrame = 0;
             m_uExecutedLoops++;
         }
         t = fmodf(t, 1.0f);
     }
     CCArray* frames = m_pAnimation->getFrames();
     unsigned  int  numberOfFrames = frames->count();
     CCSpriteFrame *frameToDisplay = NULL;
     for ( unsigned  int  i=m_nNextFrame; i < numberOfFrames; i++ ) {
         float  splitTime = m_pSplitTimes->at(i);
         if ( splitTime <= t ) {
             CCAnimationFrame* frame = (CCAnimationFrame*)frames->objectAtIndex(i);
             frameToDisplay = frame->getSpriteFrame();
             ((CCSprite*)m_pTarget)->setDisplayFrame(frameToDisplay);
             m_nNextFrame = i+1;
         }
         else  {
             break ;
         }
     }
}

但并不是每次调用都需要做动画的显示更新,也可能一次调用要设置多于一次显示更新,这跟游戏的帧频率和动画的帧频率相关。如下图所示,如果动画帧比游戏帧要快,那么有可能一个游戏帧会覆盖多个动画帧;如果动画帧比游戏帧要慢,那么可能并不是每个游戏帧都需要去处理动画帧。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值