cocos2d-x Action实现原理

Action是一个非常低阶的动画系统,它通过在一段时间内对Node元素的某些属性进行插值计算。
Action的用法就不讲了,现在我想对Action的整个实现以及执行流程分析。
cocos2d-x Action实现原理_第1张图片

动作类的基类是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运动时,这个子弹的方向始终指向运动方向。

你可能感兴趣的:(cocos,Action)