1. 程式人生 > >cocos2d-x實現多個精靈動畫同步播放(一)

cocos2d-x實現多個精靈動畫同步播放(一)

    2D遊戲經常有角色穿裝備的情況,如下圖角色手部加了一個武器.此外還有格鬥遊戲裡常有的投技:

       
     注意角色是處在站立狀態下的,有Idle動畫,手部武器也要隨角色一起聯動。我們是不是要讓美術再畫一套加手部動畫的素材,那美術顯然不幹了,那要有腳呢,披風呢?不要畫死了。他們只會給你一套純武器的站立動畫,讓你自己去拼。
      那我們要想讓武器隨角色一起聯動,自然想到設定好位置和zorder後,呼叫CCSpawn同時動作的方法。可這有個大問題,就是獨立執行兩個不同的動畫會有很大機率產生不同步的問題。為了解決這一問題,必須實現一種動畫組的機制,就是讓人物作為動畫組的主動畫,武器作為動畫組的子成員,當主動畫幀切換時子動畫才切換。也就是我動你才你,要動一起動。
  
      如這個機器人是由頭上的煙和身體以及腰上的亮點組成的,攻擊時機器人對攻擊動畫同時煙也有自己的動作,煙要隨著機器人的每幀動作切換時它也要同步切換到下一幀,這時機器人作為動畫組的主成員,而煙動畫需要作為動畫組子動畫成員。

     實現同步動畫原理是CCAnimate的update方法是每執行一次就切換一次顯示幀來實現動畫效果,我們要重寫這個update,讓主動畫update時也讓動畫組的所有成員也切換關鍵幀,這樣就能實現絕對同步了。
      首先實現動畫組成員的方法 AnimateMember,繼承於CCObject
     

#ifndef _AnimationMember_
#define _AnimationMember_

#include "cocos2d.h"

class AnimationMember : public cocos2d::CCObject
{
public:
	AnimationMember();
	~AnimationMember();

	static AnimationMember* memberWithAnimation(cocos2d::CCAnimation *animation, cocos2d::CCSprite *target);
	bool initWithAnimation(cocos2d::CCAnimation *animation, cocos2d::CCSprite *target); //用動畫和播放物件來初始化

	void start();   //開始播放動畫
	void stop();	//停止播放動畫
	void setFrame(int frameIndex); //設定播放動畫的物件(_target)圖片為動畫中的某一幀
protected:
	cocos2d::CCSpriteFrame* _origFrame;  //初始幀
	cocos2d::CCAnimation* _animation;	 //動畫
	cocos2d::CCSprite *_target;			 //誰在播放動畫
private:
};
#endif
這是標頭檔案,有初始幀,動畫和目標這幾個關鍵方法。看下初始化的實現
AnimationMember* AnimationMember::memberWithAnimation(cocos2d::CCAnimation *animation, cocos2d::CCSprite *target)
{
	AnimationMember* pRet = new AnimationMember();
	if (pRet && pRet->initWithAnimation(animation, target))
	{
		return pRet;
	}
	else
	{
		delete pRet;
		pRet = NULL;
		return pRet;
	}
}

bool AnimationMember::initWithAnimation(cocos2d::CCAnimation *animation, cocos2d::CCSprite *target)
{
	bool bRet = false;
	do 
	{
		//CC_BREAK_IF(!)
		this->_animation = animation;
		this->_target = target;
		this->_animation->retain();
		this->_target->retain();
		_origFrame = NULL;
		bRet = true;
	} while (0);

	return bRet;
}
初始化只不過將幾個關鍵資訊賦值,是非常簡單的。
再看start 和 stop函式
void AnimationMember::start()
{
	_origFrame = _target->displayFrame(); //取得當前顯示的幀作為初始幀
}

void AnimationMember::stop()
{
	bool bRestore = _animation->getRestoreOriginalFrame(); //播放完成後是否恢復第一幀
	if (bRestore)
	{
		_target->setDisplayFrame(_origFrame); //恢復第一幀
	}
}
start和stop函式只是設定下初始幀,跟播放沒有關係。別急,接著往下看。
setFrame函式:
void AnimationMember::setFrame(int frameIndex)
{
	CCArray* frames = _animation->getFrames();
	int nCount = frames->count();
	if (frameIndex>=nCount)
	{
		CCLog("AnimationMember setFrame frameindex is greater than framecount");
		return;
	}
	//從動畫裡取得index幀
	CCAnimationFrame *frame = (CCAnimationFrame *)(frames->objectAtIndex(frameIndex));
	CCSpriteFrame *spriteFrame = frame->getSpriteFrame();
	_target->setDisplayFrame(spriteFrame);
}
setFrame是從動畫中取得想要播放的幀,然後設定成當前顯示的幀。此方法在以後會用到.
其他的還有構造和解構函式
AnimationMember::AnimationMember()
{
	_target = NULL;
	_origFrame = NULL;
	_animation = NULL;
}

AnimationMember::~AnimationMember()
{
	CC_SAFE_RELEASE_NULL(_animation);
	CC_SAFE_RELEASE_NULL(_target);
}

再來看動畫組AnimateGroup類,動畫組是用來播放動畫的,所以它要繼承於CCAnimate類,因此它具有CCAnimate的一切功能,標頭檔案如下

#ifndef _AnimateGroup_
#define _AnimateGroup_

#include "cocos2d.h"

class AnimateGroup : public cocos2d::CCAnimate
{
public:
	AnimateGroup();
	~AnimateGroup();
	//用陣列來初始化函式 
	static AnimateGroup* actionWithAnimation(cocos2d::CCAnimation *animation, cocos2d::CCArray *members);
	bool initWithAnimation(cocos2d::CCAnimation *animation, cocos2d::CCArray *members); //用動畫和陣列來初始化
	//用成員數來初始化
	static AnimateGroup* actionWithAnimation(cocos2d::CCAnimation *animation, int memberCount);
	bool initWithAnimation(cocos2d::CCAnimation *animation, int memberCount); //用動畫和陣列數來初始化

	void startWithTarget(cocos2d::CCNode *pTarget);
	void stop();  //所有動畫停止

	void update(float dt);

	cocos2d::CCArray* _members;  //動畫成員
protected:
	
};
#endif
可看到它的重要的成員變數是_members動畫成員陣列, 此外還有update方法, 看下初始化的實現
建構函式:
AnimateGroup::AnimateGroup()
{
	_members = NULL;
}

AnimateGroup::~AnimateGroup()
{
	CC_SAFE_RELEASE_NULL(_members);
}
根據陣列初始化函式:
AnimateGroup* AnimateGroup::actionWithAnimation(cocos2d::CCAnimation *animation, cocos2d::CCArray *members)
{
	AnimateGroup* pRet = new AnimateGroup();
	if (pRet && pRet->initWithAnimation(animation, members))
	{
		return pRet;
	}
	else
	{
		delete pRet;
		pRet = NULL;
		return pRet;
	}
}

bool AnimateGroup::initWithAnimation(cocos2d::CCAnimation *animation, cocos2d::CCArray *members)
{
	bool bRet = false;
	do 
	{
		CC_BREAK_IF(!CCAnimate::initWithAnimation(animation));
		this->_members = members;
		this->_members->retain();
		bRet = true;
	} while (0);

	return bRet;
}
可看出成員_members是直接傳過來的,下面是另一個初始化函式
AnimateGroup* AnimateGroup::actionWithAnimation(cocos2d::CCAnimation *animation,int memberCount)
{
	AnimateGroup* pRet = new AnimateGroup();
	if (pRet && pRet->initWithAnimation(animation, memberCount))
	{
		return pRet;
	}
	else
	{
		delete pRet;
		pRet = NULL;
		return pRet;
	}
}

bool AnimateGroup::initWithAnimation(cocos2d::CCAnimation *animation, int memberCount)
{
	bool bRet = false;

	do 
	{
		CC_BREAK_IF(!CCAnimate::initWithAnimation(animation));

		this->_members = CCArray::createWithCapacity(memberCount);
		this->_members->retain();
		
		bRet = true;
	} while (0);

	return bRet;
}
這裡只是建立個容量為指定大小的空陣列。
再看下重要的開始播放和停止播放函式
void AnimateGroup::startWithTarget(CCNode *pTarget)
{
	CCAnimate::startWithTarget(pTarget);

	AnimationMember* aniMember = NULL;
	CCObject *member = NULL;
	CCARRAY_FOREACH(this->_members, member)
	{
		aniMember = (AnimationMember*)member;
		aniMember->start();
	}
}

void AnimateGroup::stop()
{
	CCAnimate::stop();

	AnimationMember *aniMember = NULL;
	CCObject* member = NULL;
	CCARRAY_FOREACH(_members, member)
	{
		aniMember = (AnimationMember *)member;
		aniMember->stop();
	}
}
可以看出開始播放和停止播放都是開始先呼叫基類的方法,再輪循呼叫每一個子成員的開始和停止方法, 由於 開始播放和停止播放都是基類來完成,所以子成員要作的工作僅僅是設定下當前顯示的幀就行了。
可能同學們還不明白了,主動畫是CCAnimate, 而子動畫將來我們也是CCAnimate並把它放入_members裡,那麼AnimateMember類的start函式沒有呼叫CCAnimate::start和stop函式,那我們怎麼讓子動畫開始播放呢?不錯,這是個問題,我們想讓子動畫與動畫同步播放,就不能再呼叫CCAnimate的start方法來開始播放動畫,因為那樣會產生不同步的問題,我們要採用最原始的辦法,直接設定幀圖片的辦法, 通過AnimateGroup的update呼叫 子動畫的 setFrame來實現.如下:
void AnimateGroup::update(float dt)
{
	CCAnimate::update(dt);

	int frameIndex = MAX(0, m_nNextFrame - 1);

	AnimationMember *aniMember = NULL;
	CCObject* member = NULL;
	CCARRAY_FOREACH(_members, member)
	{
		aniMember = (AnimationMember *)member;
		aniMember->setFrame(frameIndex);
	}
}
如果你檢視CCAnimate的update原始碼實現,你會發現它也是在update裡通過設定切換幀來實現動畫效果,所以我們也如法泡製,在update裡輪循每個動畫成員,讓它切換一下幀,注意m_nNextFrame是CCAnimate裡的protected成員變數,表示要播放的下一幀索引。
這樣我們的動畫聯動類也就實現了,一遍下來發現原理也不復雜,就是在update裡讓子動畫每幀切換下動畫,那我們怎麼運用它呢?

由於源工程比較巨集大,不可能把所有的程式碼都貼出,我自己是個菜鳥,經常被所謂的高手們嘲笑,但我相信只要瞭解了原理,就算是像我這樣智商一般的菜鳥也能運用自如:
好了不多廢話,組建個動畫還挺麻煩的,為了清晰起見寫個方法:  animateGroupWithActionWord 。
假定我們有個機器人類,繼承於CCSprite,它有悠閒,攻擊和行走各種動作,由於有它頭上冒的煙所以每一個動畫都應該是AnimateGroup, 方法如下:
AnimateGroup* Robot::animateGroupWithActionWord(const char* actionKeyWord, int frameCount, float delay)
{
        //根據frame的字首名來組建基本動畫
	CCAnimation* baseAnimation = this->animationWithPrefix(CCString::createWithFormat("robot_base_%s",actionKeyWord)->getCString(), 0, frameCount,delay);

	//腰帶動畫
	AnimationMember *beltMember = this->animationMemberWithPrefix(CCString::createWithFormat("robot_belt_%s", actionKeyWord)->getCString(), 0, frameCount, delay, _belt);
	//頭上的煙動畫
	AnimationMember *smokeMember = this->animationMemberWithPrefix(CCString::createWithFormat("robot_smoke_%s", actionKeyWord)->getCString(), 0, frameCount, delay, _smoke);
<span style="white-space:pre">	</span>//組建動畫組成員 將腰帶動畫和煙動畫放進去
	CCArray *animationMembers = CCArray::create();
	animationMembers->addObject(beltMember);
	animationMembers->addObject(smokeMember);
<span style="white-space:pre">	</span>//生成動畫組
	return AnimateGroup::actionWithAnimation(baseAnimation, animationMembers);
}
註釋寫的很清楚,就是生成三個基本動畫,將機器人的動畫作為主動畫(actionWithAnimation作為第一個引數傳入,非常重要),其他兩個作為子成員塞進_members裡。等等,那個討厭的animationMemberWithPrefix是什麼?其實也就是將動畫生成的一些步驟封裝了一下,程式碼如下:
AnimationMember* ActionSprite::animationMemberWithPrefix(const char* prefix, int startFrameIdx, int frameCount, float delay, cocos2d::CCSprite* target)
{
	CCAnimation* animation = this->animationWithPrefix(prefix, startFrameIdx, frameCount, delay);
	return AnimationMember::memberWithAnimation(animation, target);
}
   疑?怎麼還有一層animationWithPrefix封裝,煩不煩呀,沒辦法,源工程程式碼就是這樣寫的,我也是拿來主義,這個方法才是真正的動畫封裝,意思是從plist中取出幀名字首為prefix的幀, 根據開始索引號和結束索引號在程式碼裡拼出幀名,如"robot_idle_00.png", "robot_idle_01.png",結合每幀延時delay來生成動畫。具體程式碼如下:
  
CCAnimation* ActionSprite::animationWithPrefix(const char* prefix, int startFrameIdx, int frameCount, float delay)
{
	int idxCount = frameCount + startFrameIdx; //總幀數
	CCArray *frames = CCArray::createWithCapacity(frameCount);
	CCSpriteFrame *frame;
	for (int i=startFrameIdx; i<idxCount; i++)
	{
		frame = CCSpriteFrameCache::sharedSpriteFrameCache()->spriteFrameByName(CCString::createWithFormat("%s_%02d.png", prefix, i)->getCString());
		frames->addObject(frame);
	}

	return CCAnimation::createWithSpriteFrames(frames, delay);
}
這個程式碼您一定很熟悉,是cocos2d-x的標準的動畫生成步驟,不多解釋,它返回的是CCAnimation。
具體運用它就很簡單了。
例如機器人的站立動畫:
//idle動畫
AnimateGroup *idleAnimationGroup = this->animateGroupWithActionWord("idle", 5, 1.0f/12.0f);
this->_idleAction = CCRepeatForever::create(idleAnimationGroup)
this->idleAction->retain();
這個機器人Robot類裡專門有個成員變數是_idleAction,用來存放站立動畫,要播放時直接:
robot->runAction(robot->_idleAction); 即可
  可以看出,執行正常
總結:雖然運用它看起來很麻煩,步驟很煩瑣,但我一直不相信簡單就是美這種膚淺的話,要想實現複雜的功能,光想著簡單是沒有用的。不過基本原理確實不復雜,其實大量的程式碼都是基本的生成幀動畫。動畫組的主要步驟就是先生成主動畫和子動畫,用主動畫來初始化動畫組,子動畫塞到動畫組的_members數組裡,然後就可以像正常的CCAnimate這樣來播放了。

機器人的例子是完結了,相信讀者看後可以運用的自己的工程中,但是別以為大功告成了。因為這個例子不具有代表性,因為機器人頭上的煙本身就是以屬於機器人類裡的,而我們經常遇到的角色穿裝備,還有格鬥遊戲裡的投技,子物件就和主物件不是一個類的包含關係,而是兩個獨立的物件,有自己的位置和朝向,這時就需要考慮位置和朝向的關係了,不然會發生動畫是聯動了但位置卻對的亂七八糟這種情況。這個放在下一節中講解。