1、动做机制的用法node
在深刻学习动做机制在 Cocos2d-x 里是如何实现的以前,咱们先来学习整套动做机制的用法,先知道怎么用,再深刻学习它如何实现,是一个很好很重要的学习方法。编程
(1)基本概念数组
CCAction 是动做类的基类,全部的动做都派生自这个类,它建立的一个对象表明了一个动做。动做做用于 CCNode,所以,任何一个动做都须要由 CCNode 对象来执行。CCAction 做为一个基类,其实质是一个接口(即抽象类),由它派生的实现类(如运动和转动等)才是咱们实际使用的动做。CCAction 的绝大多数实现类都派生自 CCFiniteTimeAction,这个类定义了在有限时间内能够完成的动做。CCFiniteTimeAction 定义了 reverse 方法,经过这个方法能够得到一个与原动做相反的动做(称做逆动做),例如隐藏一个精灵后,用逆转动做再显示出来。固然,并不是全部的动做都有对应的逆动做,例如相似"放大到"等设置属性为常量的动做不存在逆动做,而设置属性为相对值的动做则每每存在相应的逆动做。由 CCFiniteTimeAction 派生出的两个主要类分别是瞬时动做(CCActionInstant)和持续性动做(CCActionInterval),这两类动做后面介绍的复合动做配合使用,能获得复杂生动的动做效果。安全
(2)瞬时动做less
瞬时动做是指能马上完成的动做,是 CCFiniteTimeAction 中动做持续时间为 0 的特例。下面介绍一些经常使用的瞬时动做:ide
1. CCPlace:将节点放置到某个指定位置,其做用与修改节点的 Position 属性相同。函数
2. CCFlipX 和 CCFlipY:这两个动做分别用于将精灵沿 X 和 Y 轴反向显示,其做用与设置精灵的 FlipX 和 FlipY 属性相同。oop
3. CCShow 和 CCHide:这两个动做分别用于显示和隐藏节点,其做用与设置节点的 Visible 属性的做用同样。学习
4. CCCallFunc:动画
CCCallFunc 系列动做包括 CCCallFunc、CCCallFuncN、CCCallFuncND,以及 CCCall- FuncO 四个动做,用来在动做中进行方法的调用(之因此不是函数调用,是由于它们只能调用某个类中的实例方法,而不能调用普通的 C 函数)。当某个对象执行 CCCallFunc 系列动做时,就会调用一个先前被设置好的方法,以完成某些特别的功能。后面咱们将举例说明它的用途。在 CCCallFunc 系列动做的 4 个类中,CCCallFunc 调用的方法不包含参数,CCCallFuncN 调用的方法包含一个 CCNode*类型的参数,表示执行动做的对象。CCCallFuncND 调用的方法包含两个参数,不只有一个节点参数,还有一个自定义参数(CCNode*与 void*)。CCCallFuncO 调用的方法则只包含一个 CCObject*类型的参数。
实际上,CCCallFunc 系列动做的后缀"N"表示 Node 参数,指的是执行动做的对象,"D"表示 Data 参数,指的是用户自定义的数据,"O"表示对象,指的是一个用户自定义的 CCObject 参数。在不一样的状况下,咱们能够根据不一样的需求来选择不一样的CCCallFunc 动做。考虑一种状况,咱们建立了许多会在屏幕中移动的精灵,但愿精灵在移动结束以后就从游戏中删除。
为了实现这个效果,咱们能够建立一系列动做:首先让精灵移动,而后调用一个 removeSelf(CCNode* nodeToRemove)方法来删除 nodeToRemove 对象。在 removeSelf 方法中须要访问执行此动做的精灵,所以咱们就采用 CCCallFuncN 来调用removeSelf 方法。
(3)持续性动做
持续性动做是在持续的一段时间里逐渐完成的动做,每一种持续性动做一般都存在两个不一样的变种动做,分别具备 To 和 By 后缀:后缀为 To 的动做描述了节点属性值的绝对变化,例如 CCMoveTo 将对象移动到一个特定的位置;然后缀为 By 的动做则描述了属性值相对的变化,如 CCMoveBy 将对象移动一段相对位移。
根据做用效果不一样,能够将持续性动做划分为如下 4 大类:
1. 位置变化动做:CCMoveTo 和 CCMoveBy,CCJumpTo 和 CCJumpBy,CCBezierTo 和 CCBezierBy。
2. 属性变化动做:CCScaleTo 和 CCScaleBy,CCRotateTo 和 CCRotateBy,CCFadeIn 和 CCFadeOut,CCFadeTo。
3. 视觉特效动做:CCBlink,CCAnimation(播放帧动画,用帧动画的形式实现动画效果)。
4. 控制动做: CCDelayTime、CCRepeat 和CCRepeatForever 等,控制动做与复合动做息息相关。
(4)复合动做
1 重复(CCRepeat/CCRepeatForever):
CCRepeat* CCRepeat::create(CCFiniteTimeAction *pAction, unsigned int times); CCRepeatForever *CCRepeatForever::create(CCActionInterval *pAction);
2 并列(CCSpawn):
CCSpawn::create(CCFiniteTimeAction *pAction1,...);
CCSpawn::create(CCFiniteTimeAction *pAction1, CCFiniteTimeAction *pAction2);
3. 序列(CCSequence):
CCSequence::create(CCFiniteTimeAction *pAction1,...);
CCSequence::create(CCFiniteTimeAction *pAction1,CCFiniteTimeAction *pAction2);
在实现 CCSequence 和 CCSpawn 两个组合动做类时,有一个很是有趣的细节:成员变量中并无定义一个可变长的容器来容纳每个动做系列,而是定义了m_pOne和m_pTwo两个动做成员变量。若是咱们建立了两个动做的组合,那么m_pOne与m_pTwo就分别是这两个动做自己;当咱们建立更多动做的组合时,引擎会把动做分解为两部分来看待,其中后一部分只包含最后一个动做,而前一部分包含它以前的全部动做,引擎把 m_pTwo 设置为后一部分的动做,把 m_pOne 设置为其他全部动做的组合。例如,语句 sequence = CCSequence::create(action1, action2, action3, action4, NULL);就等价于:
CCSequence s1 = CCSequence::createWithTwoActions(action1, action2); CCSequence s2 = CCSequence::createWithTwoActions(s1, action3); sequence = CCSequence::createWithTwoActions(s2, action4);
CCSpawn 与 CCSequence 所采用的机制相似。下面是 CCSpawn 的一个初始化方法,就是利用递归的思想简化了编程的复杂度:
CCFiniteTimeAction* CCSpawn::create(CCArray *arrayOfActions) { CCFiniteTimeAction* prev = (CCFiniteTimeAction*)arrayOfActions->objectAtIndex(0); for (unsigned int i = 1; i < arrayOfActions->count(); ++i) { prev = create(prev, (CCFiniteTimeAction*)arrayOfActions->objectAtIndex(i)); } return prev; }
4. 延时(CCDelayTime):CCDelayTime::create(float d);
(5)变速动做
变速动做包括 CCSpeed 动做与 CCEase 系列动做:
1. CCSpeed:CCSpeed 用于线性地改变某个动做的速度,所以,能够实现成倍地快放或慢放功能。为了改变一个动做的速度,首先须要将目标动做包装到 CCSpeed 动做中:
CCRepeatForever* repeat = CCRepeatForever::create(animation); CCSpeed* speed = CCSpeed::create(repeat, 1.0f); speed->setTag(action_speed_tag); fish->runAction(speed);
接下来,在须要改变速度的地方,咱们经过修改变速动做的 speed 属性来改变更做速度,下面的代码将会把上面设置的动画速度变为原来的两倍:
CCSpeed * speed = fish->getActionByTag(action_speed_tag); speed->setSpeed(2.0f);
2. CCActionEase:
虽然使用 CCSpeed 可以改变更做的速度,然而它只能按比例改变目标动做的速度。若是咱们要实现动做由快到慢、速度随时间改变的变速运动,须要不停地修改它的speed属性才能实现,显然这是一个很烦琐的方法。下面将要介绍的CCActionEase系列动做经过使用内置的多种自动速度变化来解决这一问题。CCActionEase 系列包含 15 个动做,它们能够被归纳为 5 类动做:指数缓冲、Sine缓冲、弹性缓冲、跳跃缓冲和回震缓冲。每一类动做都有 3 个不一样时期的变换:In、Out 和 InOut。
CCActionEase 的使用方法与 CCSpeed 相似。以 Sine 缓冲为例,如下代码实现了 InSine 变速运动:
CCEaseSineIn* sineIn = CCEaseSineIn::create(action); sineIn->setTag(action_sine_in_tag); fish->runAction(sineIn);
(6)建立自定义动做
CCAction 包含两个重要的方法:step 与 update。step 方法会在每一帧动做更新时触发,该方法接受一个表示调用时间间隔的参数 dt,dt 的积累即为动做运行的总时间。引擎利用积累时间来计算动做运行的进度(一个从 0 到 1 的实数),并调用 update 方法更新动做。update 方法是 CCAction 的核心,它由 step 方法调用,接受一个表示动做进度的参数,每个动做都须要利用进度值改变目标节点的属性或执行其余指令。自定义动做只须要从这两个方法入手便可,咱们一般只须要修改 update 方法就能够实现简单的动做。
也许有的读者已经有了疑问,step 方法与 update 方法均可以作到每一帧判断一次方向,为何选择重载 step 方法而不是update 方法呢?这是由于引擎在 step 方法中对动做对象的内部成员进行了更新,更新后才会由此方法调用 update 方法来更新目标节点。在方向追踪的动做中,咱们除了在每一帧判断方向,还必须同步执行被包装的动做。这就须要咱们调用被包装动做的 step 方法,以保证对象可以被完整地更新。
2、动做机制实现原理
(1)动做类的结构
class CC_DLL CCAction : public CCObject { public: CCAction(void); virtual ~CCAction(void); const char* description(); virtual CCObject* copyWithZone(CCZone *pZone); //! return true if the action has finished virtual bool isDone(void); //! called before the action start. It will also set the target. virtual void startWithTarget(CCNode *pTarget); /** called after the action has finished. It will set the 'target' to nil. IMPORTANT: You should never call "[action stop]" manually. Instead, use: "target->stopAction(action);" */ virtual void stop(void); //! called every frame with it's delta time. DON'T override unless you know what you are doing. virtual void step(float dt); /** called once per frame. time a value between 0 and 1 For example: - 0 means that the action just started - 0.5 means that the action is in the middle - 1 means that the action is over */ virtual void update(float time); inline CCNode* getTarget(void) { return m_pTarget; } /** The action will modify the target properties. */ inline void setTarget(CCNode *pTarget) { m_pTarget = pTarget; } inline CCNode* getOriginalTarget(void) { return m_pOriginalTarget; } /** Set the original target, since target can be nil. Is the target that were used to run the action. Unless you are doing something complex, like CCActionManager, you should NOT call this method. The target is 'assigned', it is not 'retained'. @since v0.8.2 */ inline void setOriginalTarget(CCNode *pOriginalTarget) { m_pOriginalTarget = pOriginalTarget; } inline int getTag(void) { return m_nTag; } inline void setTag(int nTag) { m_nTag = nTag; } public: /** Allocates and initializes the action @deprecated: Please use create() instead. This interface will be deprecated sooner or later. */ CC_DEPRECATED_ATTRIBUTE static CCAction* action(); /** Create an action */ static CCAction* create(); protected: CCNode *m_pOriginalTarget; /** The "target". The target will be set with the 'startWithTarget' method. When the 'stop' method is called, target will be set to nil. The target is 'assigned', it is not 'retained'. */ CCNode *m_pTarget; /** The action tag. An identifier of the action */ int m_nTag; };
继承自 CCAction 的 CCFiniteTimeAction 主要新增了一个用于保存该动做总的完成时间的成员变量:ccTime m_fDuration。对于 CCFiniteTimeAction 的两个子类 CCActionInstant 和 CCActionInterval,前者没有新增任何函数和变量,然后者增长了两个成员变量--ccTime m_elapsed 和 bool m_bFirstTick,其中 m_elapsed 是从动做开始起逝去的时间,而 m_bFirstTick是一个控制变量,在后面的分析中,咱们将看到它的做用。
(2)动做的更新
1. 当咱们对 CCNode 调用 runAction(CCAction* action)方法时,动做管理类 CCActionManager(它是一个单例对象)会将新的 CCAction 和对应的目标节点添加到其管理的动做表中。在 CCActionManager 的 addAction 方法中,咱们将动做添加到动做队列以后,就会对该 CCAction 调用成员函数startWithTarget(CCNode* pTarget)来绑定该动做的执行者。而在 CCAction 的子类中(如 CCActionInterval),还初始化了一些参数:
void CCActionInterval::startWithTarget(CCNode *pTarget) { CCFiniteTimeAction::startWithTarget(pTarget); m_elapsed = 0.0f; m_bFirstTick = true; }
2. 当这些准备工做都完成后,每一帧刷新屏幕时,系统都会在 CCActionManager 中遍历其动做表中的每个动做,并调用该动做的 step(ccTimedt)方法。step 方法主要负责计算 m_elapsed 的值,并调用 update(float time)方法,相关代码以下:
void CCActionInterval::step(float dt) { if (m_bFirstTick) { m_bFirstTick = false; m_elapsed = 0; } else { m_elapsed += dt; } this->update(MAX (0,
MIN(1, m_elapsed / MAX(m_fDuration, FLT_EPSILON)) ) ); }
传入 update 方法的 time 参数表示逝去的时间与动做完成须要的时间的比值,是介于 0 和 1 之间的一个数,即动做完成的百分比。
CCActionInterval并无进一步实现update方法。下面咱们继续以继承自CCActionInterval的CCRotateTo动做的update方法为例,分析 update 函数是如何实现的,其实现代码以下:
void CCRotateTo::update(float time) { if (m_pTarget) { m_pTarget->setRotation(m_fStartAngle + m_fDiffAngle * time); } }
看到这里,咱们已经能看出 Cocos2d-x 的动做机制的整个工做流程了。在 CCRotateTo 中,最终完成的操做是修改目标节点的 Rotation 属性值,更新该目标节点的旋转属性值。
3. 最后,在每一帧刷新结束后,在 CCActionManager 类的 update 方法中都会检查动做队列中每个动做的 isDone 函数是否返回 true。若是返回 true,则动做已完成,将其从队列中删除。isDone 函数的代码以下:
bool CCActionInterval::isDone(void) { return m_elapsed >= m_fDuration; }
对于不一样的动做类,虽然总体流程大体都是先调用 step 方法,而后按照各个动做的具体定义来更新目标节点的属性,可是不一样动做的具体实现会有所不一样。例如,CCRepeatForever 动做的 isDone 函数始终返回 false,由于它是永远在执行的动做;又如CCActionInstant 及其子类的 step 函数中,向 update 传递的参数值始终是 1,由于瞬时动做会在下一帧刷新后完成,不须要屡次执行 update。
(3)CCActionManager 的工做原理
学习了 CCAction 在每一帧中如何被更新以后,咱们不妨回头看看动做管理类 CCActionManager 的工做原理。在对CCDirector 进行初始化时,也会对 CCActionManager 进行初始化。下面的代码是 CCDirector::init()方法中的一部分:
// action manager m_pActionManager = new CCActionManager(); m_pScheduler->scheduleUpdateForTarget(m_pActionManager, kCCPrioritySystem, false);
能够看到,在 CCActionManager 被初始化后,立刻就调用了定时调度器 CCScheduler 的 scheduleUpdateForTarget 方法。在 scheduleUpdateForTarget 函数中,咱们为 CCActionManager 注册了一个按期更新的服务,这意味着动做的调度与定时器的调度都统一受到 CCScheduler 的控制。具体地说,咱们能够方便地同时暂停或恢复定时器与动做的运行,而没必要考虑它们不一样步的问题。
CCScheduler 在每一帧更新时,都会触发 CCActionManager 注册的 update 方法。从下面给出的 CCActionManager::update方法的代码能够看到,CCActionManager 在这时对每个动做都进行了更新。与调度器 CCScheduler 相似的一点是,为了防止动做调度过程当中所遍历的表被修改,Cocos2d-x 对动做的删除进行了仔细地处理,保证任何状况下均可以安全地删除动做:
// main loop void CCActionManager::update(float dt) { for (tHashElement *elt = m_pTargets; elt != NULL; ) { m_pCurrentTarget = elt; m_bCurrentTargetSalvaged = false; if (! m_pCurrentTarget->paused) { // The 'actions' CCMutableArray may change while inside this loop. for (m_pCurrentTarget->actionIndex = 0; m_pCurrentTarget->actionIndex < m_pCurrentTarget->actions->num; m_pCurrentTarget->actionIndex++) { m_pCurrentTarget->currentAction = (CCAction*)m_pCurrentTarget->actions->arr[m_pCurrentTarget->actionIndex]; if (m_pCurrentTarget->currentAction == NULL) { continue; } m_pCurrentTarget->currentActionSalvaged = false; m_pCurrentTarget->currentAction->step(dt); if (m_pCurrentTarget->currentActionSalvaged) { // The currentAction told the node to remove it. To prevent the action from // accidentally deallocating itself before finishing its step, we retained // it. Now that step is done, it's safe to release it. m_pCurrentTarget->currentAction->release(); } else if (m_pCurrentTarget->currentAction->isDone()) { m_pCurrentTarget->currentAction->stop(); CCAction *pAction = m_pCurrentTarget->currentAction; // Make currentAction nil to prevent removeAction from salvaging it. m_pCurrentTarget->currentAction = NULL; removeAction(pAction); } m_pCurrentTarget->currentAction = NULL; } } // elt, at this moment, is still valid // so it is safe to ask this here (issue #490) elt = (tHashElement*)(elt->hh.next); // only delete currentTarget if no actions were scheduled during the cycle (issue #481) if (m_bCurrentTargetSalvaged && m_pCurrentTarget->actions->num == 0) { deleteHashElement(m_pCurrentTarget); } } // issue #635 m_pCurrentTarget = NULL; }
3、CCSequence动做案例分析——CCSequence不能执行CCRepeatForever
(1)示例代码
CCBlink* blink=CCBlink::create(0.5f,10);//建立闪烁动画,duration=0.5s CCAnimation* animation=CCAnimation::create(); animation->addSpriteFrameWithFileName("CloseNormal.png"); animation->addSpriteFrameWithFileName("CloseSelected.png"); animation->setDelayPerUnit(1.0f);//帧间间隔1s CCAnimate* animate=CCAnimate::create(animation);//建立帧动画 CCRepeatForever* repeat=CCRepeatForever::create(animate); CCSequence* sequence=CCSequence::create(blink,repeat,NULL);//建立连续动画 CCSprite* close=CCSprite::create("CloseNormal.png"); close->setPosition(ccp(240,160)); this->addChild(close); close->runAction(sequence);//执行连续动画
结果精灵闪烁10次之后,帧动画不执行了。
(2)缘由
//建立CCSequence CCSequence* CCSequence::create(CCFiniteTimeAction *pAction1, ...)
内部调用了createWithVariableList,从实现能够看出这是一个递归调用。
//获取动做列表,建立CCSequence CCSequence* CCSequence::createWithVariableList(CCFiniteTimeAction *pAction1, va_list args) { CCFiniteTimeAction *pNow;//当前动做 CCFiniteTimeAction *pPrev = pAction1;//第一个动做 bool bOneAction = true;//只有一个动做的标志位 while (pAction1) { pNow = va_arg(args, CCFiniteTimeAction*);//获取当前动做 if (pNow)//若是存在 { pPrev = createWithTwoActions(pPrev, pNow);//用前两个动做建立CCSequence并赋给第一个动做 bOneAction = false;//置false } else//若是不存在 { // If only one action is added to CCSequence, make up a CCSequence by adding a simplest finite time action. if (bOneAction)//若是只有一个动做 { pPrev = createWithTwoActions(pPrev, ExtraAction::create()); } break;//跳出循环 } } return ((CCSequence*)pPrev);//返回第一个动做 }
假若有3个动做要被串联,则先把第1个和第2个串联一个CCSequence,再把这个CCSequence和第3个动做串联成最终的CCSequence,而后返回。从CCSequence的成员变量能够看到:
CCFiniteTimeAction *m_pActions[2];//代表只包含2个动做对象指针
使用递归多少会下降程序的运行效率,可是却能够换来代码的简洁性,一样的CCSpawn也是这么实现的。在createWithTwoActions中,调用了initWithTwoActions函数,实现了把两个动做串成一个CCSequence,关键代码以下:
float d = pActionOne->getDuration() + pActionTwo->getDuration();//获取两个动做的duration CCActionInterval::initWithDuration(d);//赋给新的CCSequence m_pActions[0] = pActionOne;//同时把两个动做赋给m_pActions指针数组 pActionOne->retain(); m_pActions[1] = pActionTwo; pActionTwo->retain();
从示例能够看出,闪烁动画blink的duration是0.5s,那CCRepeatForever呢?1s?固然不是,1s只是帧动画animate的帧间间隔,每一个帧动画包含2帧,而CCRepeatForever的duration是0。所以,当示例中的闪烁动画blink和重复动画repeat串联成CCSequence sequence的时候,sequence的duration就变成0.5+0=0.5s,这很重要。
CCSequence中有这么一个成员变量
float m_split;//记录了第一个动画时长占总时长的比例,也就是2个动画的时长分界
当执行runAction的时候,CCSequence会调用
void CCSequence::startWithTarget(CCNode *pTarget) { CCActionInterval::startWithTarget(pTarget); m_split = m_pActions[0]->getDuration() / m_fDuration;//获取第一个动画占总时长的比例 m_last = -1; }
而这里因为blink占了0.5s,repeat占了0s,总时长0.5s,因此m_split是0.5/0.5=1。blink占满了整个CCSequence。这时候再来看CCSequence::update(float dt)函数的执行,就会恍然大悟了。
int found = 0;//当前播放动做索引 float new_t = 0.0f;//新播放进度 if( t < m_split ) {//播放进度<分界进度 found = 0;//设置当前播放的是第一个动做 if( m_split != 0 )//若是第一个动做时长占比!=0 new_t = t / m_split;//计算出第一个动做新的播放进度 else new_t = 1;//设置第一个已播放完毕 } else {//播放进度>=分界进度 found = 1;//设置当前播放的是第二个动做 if ( m_split == 1 )//若是第一个动做时长占比==1 new_t = 1;//设置第二个动做已完成 else new_t = (t-m_split) / (1 - m_split );//计算出第二个动做新的播放进度 }
CCSpawn也会有这个问题,因此CCSpawn也没法执行加入其中的CCRepeatForever动做。
CCRepeatForever的反转动做也是无效了,一个不会中止的动做从什么地方开始反转?固然你能够先把动做反转了再加入CCRepeatForever中,这是没问题的。
(3)解决方案
对于同时动做,不使用CCSpawn,采用分别执行
close->runAction(blink);
close->runAction(repeat);
对于连续动做,不直接往CCSequence中加入CCRepeatForever,而是把CCRepeatForever放入瞬时动做CCCallFunc中,再把CCCallFunc加入CCSequence中执行。
close=CCSprite::create("CloseNormal.png"); CCBlink* blink=CCBlink::create(0.5f,10); CCCallFunc* callFunc=CCCallFunc::create(this,callfunc_selector(TestScene::repeatFunc));//建立CCCallFunc对象 CCSequence* sequence=CCSequence::create(blink,callFunc,NULL);//把CCCallFunc对象加入CCSequence中 close->setPosition(ccp(240,160)); this->addChild(close); close->runAction(sequence); void TestScene::repeatFunc() { CCAnimation* animation=CCAnimation::create(); animation->addSpriteFrameWithFileName("CloseNormal.png"); animation->addSpriteFrameWithFileName("CloseSelected.png"); animation->setDelayPerUnit(1.0f); CCAnimate* animate=CCAnimate::create(animation); CCRepeatForever* repeat=CCRepeatForever::create(animate); close->runAction(repeat);
对于CCAnimation帧动画,能够设置循环属性,而不使用CCRepeatForever。
animation->setLoops(-1);
(4)虽然CCRepeatForever也一样继承于CCActionInterval,理论上是延时动做的子类,可是和通常的延时动做又有很大的不一样,因此平时在使用的时候必须很当心,不能当成通常的CCActionInterval使用。