首页 > 代码库 > 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这样来播放了。

机器人的例子是完结了,相信读者看后可以运用的自己的工程中,但是别以为大功告成了。因为这个例子不具有代表性,因为机器人头上的烟本身就是以属于机器人类里的,而我们经常遇到的角色穿装备,还有格斗游戏里的投技,子对象就和主对象不是一个类的包含关系,而是两个独立的对象,有自己的位置和朝向,这时就需要考虑位置和朝向的关系了,不然会发生动画是联动了但位置却对的乱七八糟这种情况。这个放在下一节中讲解。