action是cocos2dx扮演中很重要的角色,很多特殊的效果,都是通过他来实现,而且通过他可以方便的产生很多效果,
而不需要太多的相关知识储备、以及实现技巧。借着学习的思路,我们走一下cocos2dx中action的流程分析,大家共勉
吧。
【ActionManager篇】
一般action的入口在:
Action * Node::runAction(Action* action) { CCASSERT( action != nullptr,"Argument must be non-nil"); _actionManager->addAction(action,this,!_running); return action; }
他是node一个借口,而node是cocos2dx中绝大部分类的根类。所以意味着基本所有的cocos2dx具体类,都可以调用这个接口,
里面_actionManager成员,在Node::Node()构造函数里赋值,如下
_actionManager = director->getActionManager();
从director类获取到了,而director类是cocosdx里的最基本的功能类,在其init方法里,
_actionManager = new ActionManager(); // 初始化完,马上就注册了update更新 _scheduler->scheduleUpdate(_actionManager,Scheduler::PRIORITY_SYSTEM,false);
在其初始化后,就注册了update更新事件,所以ActionManager的update方法在每帧都会调用了,达到可以更新action的作用。
接着分析ActionManager::addAction,看action是如何加入ActionManager
void ActionManager::addAction(Action *action,Node *target,bool paused) { CCASSERT(action != nullptr,""); CCASSERT(target != nullptr,""); tHashElement *element = nullptr; // we should convert it to Ref*,because we save it as Ref* Ref *tmp = target; <span style="white-space:pre"> </span>// 根据目标来查找对应的数据(tHashElement) HASH_FIND_PTR(_targets,&tmp,element); // 没有找到 <span style="white-space:pre"> </span>if (! element) { <span style="white-space:pre"> </span>// 构造元素tHashElement空间 element = (tHashElement*)calloc(sizeof(*element),1); element->paused = paused; // 这里引用了target,为了防止在更新action,其宿主释放,这里retain一下 <span style="white-space:pre"> </span>target->retain(); element->target = target; // 添加至_targets,以target的指针为key <span style="white-space:pre"> </span>HASH_ADD_PTR(_targets,target,element); } <span style="white-space:pre"> </span>// 分配element里的成员空间 actionAllocWithHashElement(element); <span style="white-space:pre"> </span>// CCASSERT(! ccArrayContainsObject(element->actions,action),""); // 将action放入element->actions <span style="white-space:pre"> </span>ccArrayAppendObject(element->actions,action); <span style="white-space:pre"> </span>// 这里设置一下action的对象 action->startWithTarget(target); }
里面涉及的方法如下:
// 分配tHashElement成员空间 void ActionManager::actionAllocWithHashElement(tHashElement *element) { // 分配action的存储空间,默认是4个 // 4 actions per Node by default if (element->actions == nullptr) { element->actions = ccArrayNew(4); }else if (element->actions->num == element->actions->max) // action满了,空间翻倍 { ccArrayDoubleCapacity(element->actions); } }
// 将action放入element void ccArrayAppendObject(ccArray *arr,Ref* object) { CCASSERT(object != nullptr,"Invalid parameter!"); // 这里将action retain了一下 object->retain(); // 放置action序列末尾 arr->arr[arr->num] = object; arr->num++; }
// 设置action的对象 void Action::startWithTarget(Node *aTarget) { _originalTarget = _target = aTarget; }
好了,到此,我们的action已经加入ActionManager中去了,现在由cocos2dx框架来驱动action了,前面已经分析了,ActionManager::update
会每帧调用,我们下面分析update
void ActionManager::update(float dt) { // 遍历_targets for (tHashElement *elt = _targets; elt != nullptr; ) { // _currentTarget = elt; _currentTargetSalvaged = false; // 该对象没有暂停 if (! _currentTarget->paused) { <span style="white-space:pre"> </span>// 遍历该对象的所有actions // 英文提示该actions可能在循环里改变 // The 'actions' MutableArray may change while inside this loop. for (_currentTarget->actionIndex = 0; _currentTarget->actionIndex < _currentTarget->actions->num; _currentTarget->actionIndex++) { // 当前action _currentTarget->currentAction = (Action*)_currentTarget->actions->arr[_currentTarget->actionIndex]; if (_currentTarget->currentAction == nullptr) { continue; } _currentTarget->currentActionSalvaged = false; // 回调action::step _currentTarget->currentAction->step(dt); if (_currentTarget->currentActionSalvaged) { // 这里action::release了,记得前面我们分析,在 // void ccArrayAppendObject(ccArray *arr,Ref* object) retain了一下 // 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. _currentTarget->currentAction->release(); } else if (_currentTarget->currentAction->isDone()) // action完成 { // 回调action::stop _currentTarget->currentAction->stop(); // 移除该action Action *action = _currentTarget->currentAction; // Make currentAction nil to prevent removeAction from salvaging it. _currentTarget->currentAction = nullptr; // 移除该action,其会改变_currentTargetSalvaged的标志, // 也会改变_currentTarget->actions->num removeAction(action); } // 清理一下当前action,_currentTarget->currentAction = nullptr; } } // elt,at this moment,is still valid // so it is safe to ask this here (issue #490) elt = (tHashElement*)(elt->hh.next); // 如果该actions没有action,并且_currentTargetSalvaged为真 // only delete currentTarget if no actions were scheduled during the cycle (issue #481) if (_currentTargetSalvaged && _currentTarget->actions->num == 0) { deleteHashElement(_currentTarget); } } // issue #635 _currentTarget = nullptr; }
简单来说,update的工作,就是遍历_targets,换句话说,就是遍历所有调用了runAction方法的Node对象,执行其Action的step方法,传入的是每帧
逝去的时间,在处理完后,看这个action是不是完成了,完成了的话,就移除该action,在最后,如果该tHashElement的actions都没有action,就移除
该tHashElement,其中里面涉及的方法分析如下:
void ActionManager::removeAction(Action *action) { // explicit null handling if (action == nullptr) { return; } // 找到该action对应的tHashElement tHashElement *element = nullptr; Ref *target = action->getOriginalTarget(); HASH_FIND_PTR(_targets,&target,element); if (element) { // 获得该action在actions的索引 auto i = ccArrayGetIndexOfObject(element->actions,action); if (i != CC_INVALID_INDEX) { // 移除该索引位的action removeActionAtIndex(i,element); } } else { CCLOG("cocos2d: removeAction: Target not found"); } }
里面涉及的方法如下:
void ActionManager::removeActionAtIndex(ssize_t index,tHashElement *element) { Action *action = (Action*)element->actions->arr[index]; // 如果该action是当前处理的action,将该action retain一下,并设置 // currentActionSalvaged 标志 if (action == element->currentAction && (! element->currentActionSalvaged)) { element->currentAction->retain(); element->currentActionSalvaged = true; } // 这里释放该索引位置的action,并拼合剩下action的位置关系 // 注意最后的参数传了true,说明要清理对象 ccArrayRemoveObjectAtIndex(element->actions,index,true); // 当前索引并移除,后面索引前移来填充,所以循环索引要回退。 // 仅仅也只有等于的情形吧 // update actionIndex in case we are in tick. looping over the actions if (element->actionIndex >= index) { element->actionIndex--; } // 移除该索引后,actions里没有action里 if (element->actions->num == 0) { // 当前处理的element,就是移除action所属的tHashElement // 意味这个tHashElement没有action了,标志其要移除出_targets if (_currentTarget == element) { // 是当前处理的tHashElement,暂缓移除,标志一下 _currentTargetSalvaged = true; } else { // 如果element不是当前处理的tHashElement,就直接移除 deleteHashElement(element); } } }
再接着:
void ccArrayRemoveObjectAtIndex(ccArray *arr,ssize_t index,bool releaSEObj/* = true*/) { // 要不要清理该对象 CCASSERT(arr && arr->num > 0 && index>=0 && index < arr->num,"Invalid index. Out of bounds"); if (releaSEObj) { CC_SAFE_RELEASE(arr->arr[index]); } // actions个数减一 arr->num--; // 该位置移除,后面的填上 ssize_t remaining = arr->num - index; if(remaining>0) { memmove((void *)&arr->arr[index],(void *)&arr->arr[index+1],remaining * sizeof(Ref*)); } }
至此,ActionManager部分,就是Aciton的框架部分,分析至此,脉络已经出来了,就是Scheduler驱动着这一切,我们只要调用runAction,
将Action交给ActionManager就好了,
【Action篇】
下面分析一下action的体系,action的怎样实现,造就了如此丰富的action效果,
下面是我重新摘除重点的action类定义
class CC_DLL Action : public Ref,public Clonable { public: /** returns a clone of action */ virtual Action* clone() const = 0; /** returns a new action that performs the exactly the reverse action */ virtual Action* reverse() const = 0; //! called before the action start. It will also set the target. virtual void startWithTarget(Node *target); //called after the action has finished. It will set the 'target' to nil. virtual void stop(); //! called every frame with it's delta time. DON'T override unless you know what you are doing. virtual void step(float dt); 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); protected: Node *_originalTarget; /** 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'. */ Node *_target; /** The action tag. An identifier of the action */ int _tag; }
透露了几个信息
1、该类继承自Clonable
class CC_DLL Clonable { public: /** returns a copy of the Ref */ virtual Clonable* clone() const = 0; /** * @js NA * @lua NA */ virtual ~Clonable() {}; }
就是定了克隆的接口,由于action的使用很频繁,所有有克隆是一个很重要的特性,
2、有reverse接口,说明reverse也是一个常备的特性,一个动作常常提供反转动作,但是也不是都会实现这个反转。
3、step是框架调用的,自己实现要慎重,后面会分析到,这个接口基本可以不用重载。
4、action的成员变量,就是有一个原始目标,还有一个目前目标,而且注明该目标是简单的赋值,不负责维护引用
这个action是个抽象类,规定了一些接口,用来为ActionManager提供操作接口。
下面看看最常用的有限时间动作,我摘除重要的部分如下:
class CC_DLL FiniteTimeAction : public Action { protected: //! duration in seconds float _duration; }
其实就是增加了一个时间段。而且还是一个抽象类,没有什么具体作用,下面看看他的具体应用,区间动作(ActionInterval)
class CC_DLL ActionInterval : public FiniteTimeAction { public: <span style="white-space:pre"> </span>// 实现了完成的条件,就是逝去时间大于动作区间 virtual bool isDone(void) const override; // 这里需要分析下,他是reverse实现的基础 virtual void step(float dt) override; protected: float _elapsed; bool _firstTick; };
这里要注意下step的实现
void ActionInterval::step(float dt) { // 第一次调用,初始化下 if (_firstTick) { _firstTick = false; // 逝去时间初始化 _elapsed = 0; } else { // 记录逝去的时间和 _elapsed += dt; } // 这个表达式表达就是,update的参数,就是逝去的时间在整个动作 // 时间的比例,而不是时间间隔了。 // _elapsed = 0,就是update(0) // _elapsed = _duration 就是update(1) this->update(MAX (0,// needed for rewind. elapsed could be negative MIN(1,_elapsed / MAX(_duration,FLT_EPSILON) // division by 0 ) ) ); }
下面分析个实例吧,Repeat的实现 Repeat的辅助数据如下:
protected: // 需要重复次数 unsigned int _times; // 已重复次数 unsigned int _total; // 重复动作时间占比,用于统计_total, float _nextDt; // 标记该动作是不是瞬时动作 bool _actionInstant; /** Inner action */ // 重复的动作 FiniteTimeAction *_innerAction;
具体说明都在注释上
void Repeat::startWithTarget(Node *target) { // 初始化已重复次数 _total = 0; // 本动作在总时间的占比 _nextDt = _innerAction->getDuration()/_duration; ActionInterval::startWithTarget(target); // 内部动作也初始化下对象 _innerAction->startWithTarget(target); }
核心实现update,如下:
void Repeat::update(float dt) { // 当前时间比例,已经超过一次动作的时间比例 if (dt >= _nextDt) { // 出现这种情况,只有卡的情况吧, // 时间比例超了动作时间占比,而次数又没有到目标 while (dt > _nextDt && _total < _times) { <span style="white-space:pre"> </span>// 目标动作直接更新完成 _innerAction->update(1.0f); // 已重复次数增加 _total++; // 动作停止接着又开始,保证目标动作的回调函数都调用到 _innerAction->stop(); _innerAction->startWithTarget(_target); // 重算下次目标占比 _nextDt = _innerAction->getDuration()/_duration * (_total+1); } // 总时间占比完成,但是次数没有达到,一般是临界情况 // fix for issue #1288,incorrect end value of repeat if(dt >= 1.0f && _total < _times) { _total++; } // don't set an instant action back or update it,it has no use because it has no duration if (!_actionInstant) { if (_total == _times) { _innerAction->update(1); _innerAction->stop(); } else // 最后一帧让他执行完 { // issue #390 prevent jerk,use right update _innerAction->update(dt - (_nextDt - _innerAction->getDuration()/_duration)); } } } else { // 目标动作执行update(单动作的时间占比), _innerAction->update(fmodf(dt * _times,1.0f)); } }以上,就是action的一点点分析,希望对大家有点帮助。