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的一点点分析,希望对大家有点帮助。