Action是一个非常低阶的动画系统,它通过在一段时间内对Node元素的某些属性进行插值计算。
Action的用法就不讲了,现在我想对Action的整个实现以及执行流程分析。
动作类的基类是Action,通过继承它可以实现很多种动作。
FiniteTimeAction:有限次动作执行类,就是按时间顺序执行一系列动作,执行完后动作结束;
Speed:调整实体(节点)的执行速度;
Follow:可以使节点跟随指定的另一个节点移动。
FiniteTimeAction又分为ActionInstanse(瞬时动作的基类)和ActionInterval(延时动作的基类)。ActionInstant:没什么特别,跟ActionInterval主要区别是没有执行过程,动作瞬间就执行完成了;ActionInterval:执行需要一定的时间(或者说一个过程)。
根据上面的类结构图,ActionInterval的子类有很多,可以通过cocos2d-x自带的tests例子来学习,主要有这些动作:移动(CCMoveTo/CCMoveBy)、缩放(ScaleTo/ScaleBy)、旋转(RotateTO/RotateBy)、扭曲(SkewTo/SkewBy)、跳跃(JumpTo/CCJumpBy)、贝塞尔曲线(BezierTo/BezierBy)、闪烁(Bink)、淡入淡出(FadeIn/FadeOut)、染色(TintTo/TintBy)等,还可以把上面这些动作的几个组合成一个序列。下面是移动和缩放动作的代码示例,其他的动作都类似,都是调用actionWithDuration函数,用到的时候具体参数的含义可以参考cocos2d-x自带的tests例子。
Action分析:
CCAction.h
class CC_DLL Action : public Ref, public Clonable
{
public:
/** Default tag used for all the actions. */
static const int INVALID_TAG = -1;
/**
* @js NA
* @lua NA
*/
virtual std::string description() const;
/** Returns a clone of action.
*
* @return A clone action.
*/
virtual Action* clone() const
{
CC_ASSERT(0);
return nullptr;
}
/** Returns a new action that performs the exactly the reverse action.
*
* @return A new action that performs the exactly the reverse action.
* @js NA
*/
virtual Action* reverse() const //逆动作
{
CC_ASSERT(0);
return nullptr;
}
/** Return true if the action has finished.
*
* @return Is true if the action has finished.
*/
virtual bool isDone() const;
/** Called before the action start. It will also set the target.
*
* @param target A certain target.
*/
virtual void startWithTarget(Node *target);//设置动作执行节点
/**
* 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();
/** Called every frame with it's delta time, dt in seconds. DON'T override unless you know what you are doing.
*
* @param dt In seconds.
*/
virtual void step(float dt); //每帧被调用 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.
*
* @param time A value between 0 and 1.
*/
virtual void update(float time);//step调用传入动作进度
/** Return certain target.
*
* @return A certain target.
*/
inline Node* getTarget() const { return _target; }
/** The action will modify the target properties.
*
* @param target A certain target.
*/
inline void setTarget(Node *target) { _target = target; }
/** Return a original Target.
*
* @return A original Target.
*/
inline Node* getOriginalTarget() const { return _originalTarget; }
/**
* 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 ActionManager, you should NOT call this method.
* The target is 'assigned', it is not 'retained'.
* @since v0.8.2
*
* @param originalTarget Is 'assigned', it is not 'retained'.
*/
inline void setOriginalTarget(Node *originalTarget) { _originalTarget = originalTarget; }
/** Returns a tag that is used to identify the action easily.
*
* @return A tag.
*/
inline int getTag() const { return _tag; }
/** Changes the tag that is used to identify the action easily.
*
* @param tag Used to identify the action easily.
*/
inline void setTag(int tag) { _tag = tag; }
CC_CONSTRUCTOR_ACCESS:
Action();
virtual ~Action();
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;
private:
CC_DISALLOW_COPY_AND_ASSIGN(Action);
};
Action作为所有动作类的基类。有限时间动作 FiniteTimeAction 集成自Action主要添加了一个变量 float _duration;保存该动作总的完成时间。
持续性动作ActionInterval 和瞬时动作ActionInstant继承自FiniteTimeAction。ActionInstant没有对FiniteTimeAction添加任何成员。
ActionInterval添加了两个变量float _elapsed;bool _firstTick;分别表示动作开始逝去的时间,和控制变量。
CCActionInterval.h
class CC_DLL ActionInterval : public FiniteTimeAction
{
public:
/** How many seconds had elapsed since the actions started to run.
*
* @return The seconds had elapsed since the ations started to run.
*/
inline float getElapsed(void) { return _elapsed; }
/** Sets the ampliture rate, extension in GridAction
*
* @param amp The ampliture rate.
*/
void setAmplitudeRate(float amp);
/** Gets the ampliture rate, extension in GridAction
*
* @return The ampliture rate.
*/
float getAmplitudeRate(void);
//
// Overrides
//
virtual bool isDone(void) const override;
/**
* @param dt in seconds
*/
virtual void step(float dt) override;
virtual void startWithTarget(Node *target) override;
virtual ActionInterval* reverse() const override
{
CC_ASSERT(0);
return nullptr;
}
virtual ActionInterval *clone() const override
{
CC_ASSERT(0);
return nullptr;
}
CC_CONSTRUCTOR_ACCESS:
/** initializes the action */
bool initWithDuration(float d);
protected:
float _elapsed; //逝去时间 是间隔时间的累加
bool _firstTick;
};
CCActionInterval.cpp
bool ActionInterval::initWithDuration(float d)
{
_duration = d; //初始化完成总时间
// prevent division by 0
// This comparison could be in step:, but it might decrease the performance
// by 3% in heavy based action games.
if (_duration == 0)
{
_duration = FLT_EPSILON;
}
_elapsed = 0; //逝去的时间
_firstTick = true; //标志
return true;
}
bool ActionInterval::isDone() const
{
return _elapsed >= _duration;
}
void ActionInterval::step(float dt)
{
if (_firstTick)
{
_firstTick = false;
_elapsed = 0;//开始执行将逝去时间设置为0
}
else
{
_elapsed += dt; //累加时间 dt为两帧的时间间隔
}
this->update(MAX (0, // needed for rewind. elapsed could be negative
MIN(1, _elapsed /
MAX(_duration, FLT_EPSILON) // division by 0
)
)
);
}
//step传入时间间隔,并调用update方法,update传入动作的完成百分比。(逝去时间/完成总时间)
void ActionInterval::setAmplitudeRate(float amp)
{
CC_UNUSED_PARAM(amp);
// Abstract class needs implementation
CCASSERT(0, "");
}
float ActionInterval::getAmplitudeRate()
{
// Abstract class needs implementation
CCASSERT(0, "");
return 0;
}
void ActionInterval::startWithTarget(Node *target)
{
FiniteTimeAction::startWithTarget(target);
_elapsed = 0.0f;
_firstTick = true;
}
ActionInstant的step()方法中向update()传入的始终为1,因为瞬时动作会在下一帧刷新后完成,不需要多次执行update方法。
动作是如何更新的呢?
当我们对CCNode 调用runAction(Action* action)的时候,动作管理类ActionManager会新的Action和对应的目标节点添加到期管理的动作类表中。
Action * Node::runAction(Action* action)
{
_actionManager->addAction(action, this, !_running);
return action;
}
在addAction中将动作添加到动作队列之后,会调用Aciton的startWithTarget方法。来绑定动作的执行者。
void ActionManager::addAction(Action *action, Node *target, bool paused)
{
tHashElement *element = nullptr;
// we should convert it to Ref*, because we save it as Ref*
Ref *tmp = target;
HASH_FIND_PTR(_targets, &tmp, element);
if (! element)
{
element = (tHashElement*)calloc(sizeof(*element), 1);
element->paused = paused;
target->retain();
element->target = target;
HASH_ADD_PTR(_targets, target, element);
}
actionAllocWithHashElement(element);
ccArrayAppendObject(element->actions, action);
action->startWithTarget(target);
}
在Director的init中ActionManager向Schedule注册了一个最高优先级的回调。然后每一帧回调更新_scheduler->update(_deltaTime);
都会调用ActionManager的update方法。
// scheduler
_scheduler = new (std::nothrow) Scheduler();
// action manager
_actionManager = new (std::nothrow) ActionManager();
_scheduler->scheduleUpdate(_actionManager,Scheduler::PRIORITY_SYSTEM, false);
在update方法中中会遍历其动作列表的每一个动作,并调用step()方法。step方法负责_elapsed的计算。
如何自定义动作?
eg1:改变动作的执行速度。分析下speed
Speed::Speed(): _speed(0.0), _innerAction(nullptr)
{}
Speed::~Speed()
{
CC_SAFE_RELEASE(_innerAction);
}
Speed* Speed::create(ActionInterval* action, float speed)
{
Speed *ret = new (std::nothrow) Speed();
if (ret && ret->initWithAction(action, speed))
{
ret->autorelease();
return ret;
}
CC_SAFE_DELETE(ret);
return nullptr;
}
bool Speed::initWithAction(ActionInterval *action, float speed)
{
CCASSERT(action != nullptr, "action must not be NULL");
action->retain();
_innerAction = action;
_speed = speed;
return true;
}
Speed *Speed::clone() const
{
// no copy constructor
auto a = new (std::nothrow) Speed();
a->initWithAction(_innerAction->clone(), _speed);
a->autorelease();
return a;
}
void Speed::startWithTarget(Node* target)
{
Action::startWithTarget(target);
_innerAction->startWithTarget(target);
}
void Speed::stop()
{
_innerAction->stop();
Action::stop();
}
void Speed::step(float dt)
{
_innerAction->step(dt * _speed);
}
bool Speed::isDone() const
{
return _innerAction->isDone();
}
Speed *Speed::reverse() const
{
return Speed::create(_innerAction->reverse(), _speed);
}
void Speed::setInnerAction(ActionInterval *action)
{
if (_innerAction != action)
{
CC_SAFE_RELEASE(_innerAction);
_innerAction = action;
CC_SAFE_RETAIN(_innerAction);
}
}
speed内部重写了step方法,他将速度参数乘以自动化的更新时间线(dt)
这样大于1的速度动作执行更快。
除了通过step来改变动画的执行时间长短,只能线性改便动画的执行速度。
另一方式是改变固定时间内的插值方程。通过改变update的时间曲线就能实现非线性变化,这类函数叫缓动函数。
例如EaseIn 只是修改了缓动函数。
void EaseIn::update(float time)
{
_inner->update(tweenfunc::easeIn(time, _rate));
}
如何实现实现动作中,执行者的角度变化,即保持运动方向和自身方向的一致性。
下面我们编写一个继承于Action的RotateAction动作。如同复合动作与变速动作一样,它会把另一个动作包装起来,在执行被包装动作的同时,设置精灵的方向。为此,我们需要在每一帧记录上一帧精灵的位置,然后再根据精灵两帧的位移确定精灵的方向。由于我们必须在RotateAction执行的同时运行被包含的目标动作,所以我们需要在step方法中调用目标动作的step方法。
.h
class RotateAction : public ActionInterval
{
public:
RotateAction();
virtual ~RotateAction();
static RotateAction* create(ActionInterval* action);
virtual void startWithTarget(Node *target) override;
virtual bool isDone() const override;
virtual RotateAction* clone() const override;
virtual RotateAction* reverse() const override;
virtual void stop() override;
virtual void update(float t) override;
inline float getDuration() const { return _innerAction->getDuration(); }
inline void setDuration(float duration) { _innerAction->setDuration(duration); }
inline float getElapsed(void) { return _innerAction->getElapsed();}
void setInnerAction(ActionInterval *action);
inline ActionInterval* getInnerAction() const { return _innerAction; }
protected:
bool initWithAction(ActionInterval* action);
ActionInterval* _innerAction;
float _startRotation;
};
RotateAction::RotateAction()
{
}
RotateAction::~RotateAction()
{
CC_SAFE_RELEASE(_innerAction);
}
RotateAction* RotateAction::create( ActionInterval* action )
{
auto ret = new (std::nothrow) RotateAction();
if (ret && ret->initWithAction(action))
{
ret->autorelease();
return ret;
}
CC_SAFE_DELETE(ret);
return nullptr;
}
bool RotateAction::initWithAction( ActionInterval* action )
{
CCASSERT(action != nullptr, "action must not be NULL");
action->retain();
_innerAction = action;
ActionInterval::initWithDuration(_innerAction->getDuration());
return true;
}
void RotateAction::startWithTarget( Node *target )
{
ActionInterval::startWithTarget(target);
_innerAction->startWithTarget(target);
_startRotation = target->getRotation();
}
bool RotateAction::isDone() const
{
return _innerAction->isDone();
}
void RotateAction::setInnerAction( ActionInterval *action )
{
if (_innerAction != action)
{
CC_SAFE_RELEASE(_innerAction);
_innerAction = action;
CC_SAFE_RETAIN(_innerAction);
}
}
RotateAction* RotateAction::clone() const
{
return RotateAction::create(_innerAction->clone());
}
RotateAction* RotateAction::reverse() const
{
return RotateAction::create(_innerAction->reverse());
}
void RotateAction::stop()
{
_innerAction->stop();
ActionInterval::stop();
}
void RotateAction::update( float t )
{
Point prePos = getTarget()->getPosition();;
_innerAction->update(t);
Point curPos = getTarget()->getPosition();
Point rotate = curPos - prePos;
float rad = rotate.getAngle();
float degree = 180.0f - CC_RADIANS_TO_DEGREES(rad);
getTarget()->setRotation(degree + _startRotation);
}
为了能够是action在执行时,target节点的方向为运动的切线方向,在RotateAction中我使用包裹其他action的方式。实现方式有点类似Speed。
例如你要实现bezier曲线运动时精灵的方向要和运动的方向一致。
auto plane = Sprite::create("Images/plane.png");
this->addChild(plane);
ccBezierConfig conf;
conf.controlPoint_1 = Vec2(0,400);
conf.controlPoint_2 = Vec2(600,400);
conf.endPosition = Vec2(600,0);
auto bezier = BezierBy::create(5.0f,conf);
plane->runAction(RotateAction::create(bezier));
图片资源如下:
如此就可以实现bezier运动时,这个子弹的方向始终指向运动方向。