action实现 cocos

2019-01-27 19:09:07 fatfish_ 阅读数 1034

自定义Action

学习自定义Action的最好方法是去查看Cocos Creator中常用动作的写法。比如cc.MoveTo继承了cc.MoveBy,而cc.MoveBy则进一步继承了cc.ActionInterval
可以从cocos-creator的引擎源码找到cc.MoveBy的相关定义:

cc.MoveBy = cc.Class({
    name: 'cc.MoveBy',
    extends: cc.ActionInterval,

    ctor:function (duration, deltaPos, deltaY) {
        this._positionDelta = cc.v2(0, 0);
        this._startPosition = cc.v2(0, 0);
        this._previousPosition = cc.v2(0, 0);

        deltaPos !== undefined && cc.MoveBy.prototype.initWithDuration.call(this, duration, deltaPos, deltaY);	
    }
    ...
})

如果需要自定义Action只需要按照自己的需求仿照cc.MoveBy的定义写一个就行了。如实现cc.ActionInterval类的initWithDurationstartWithTargetupdate方法。「sprite 圆形运动,圆周运动」给出了自定义一个圆周运动的三种方案,其中第一种方案就是集成cc.ActionInterval,自定义Action的方法。这里不再赘述。

特殊需求 - 用TypeScript自定义Action

因为自己做cocos creator时用的ts语言,在写法上与之前提到的略有不同。目的是实现精灵绕某一点旋转一定的角度。

需求

  1. 根据精灵自身旋转角度和给定半径确定精灵圆周运动的圆心。
  2. 在精灵圆周运动过程中自动调整精灵自身的旋转角度。

方案

这里给出实现的源码,并做相关解释。

const {ccclass, property} = cc._decorator;

export default class ArcMove extends cc.ActionInterval {

    name: string = "CircleMove";
    _positionDelta: cc.Vec2;
    _previousPosition: cc.Vec2;
    /**圆心 */
    _centre:cc.Vec2;
    /**半径 */
    _R: number = 0;
    /**设置节点旋转度数 */
    _angle;
    /**节点 */
    _target: cc.Node;
    /**节点在圆上的初始位置(角度)*/
    _initAngle:number = 0;
    /**圆周运动方向 counter-clockwise(逆时针方向)默认false*/
    _ccw: boolean = false;
    /**节点初始自身rotation */
    _rotation: number = 0;
    /**
     * 
     * @param duration 
     * @param R 半径
     * @param angle 绕圆心旋转角度(360度为完整圆)
     */
    constructor(duration, R, angle=360){
        super();
        this._positionDelta = cc.v2(0,0);
        this._centre  = cc.v2(0,0);
        this._previousPosition = cc.v2(0,0);
        this._R = R;
        this.initWithDuration(duration, R, angle);
    }

    initWithDuration(duration, R, angle):boolean {
        if(super["initWithDuration"](duration, this)){
            this._R = R;
            this._angle = angle;
            return true
        }
        return false;
    }

    startWithTarget(target:cc.Node) {
        super["startWithTarget"](this, target);
        this._target = target;
        // 根据节点scaleX判断圆周运动方向(顺/逆时针)
        if(target.scaleX == 1){
            this._ccw = false;
        }else{
            this._ccw = true;
        }
        // 根据节点自身倾角和半径计算圆心
        let angle = -(target.rotation + 90);
        let v1:cc.Vec2 = new cc.Vec2(Math.cos(angle/180*Math.PI), Math.sin(angle/180*Math.PI));
        let center: cc.Vec2 = v1.mul(this._R).add(target.position);
        this._centre .x = center.x;
        this._centre .y = center.y;
        // 计算节点在圆上初始位置
        this._initAngle = 90 - target.rotation;
        this._rotation = target.rotation;
        cc.log("在圆上角度:" + this._initAngle);
    }

    update (dt) {
        if (this._target) {
            var locStartPosition = this._centre ;
            
            if (cc.macro.ENABLE_STACKABLE_ACTIONS) {
                let dir: number = -1;
                if(this._ccw){
                    dir = 1;
                }
                var R = this._R;
                var rat = this._initAngle * Math.PI / 180 + dir * dt * Math.PI * 2 * (this._angle / 360);
                var x = locStartPosition.x + R * Math.cos(rat);
                var y = locStartPosition.y + R * Math.sin(rat);
                this._target.setPosition(x, y);
                this._target.rotation = this._rotation + dt * this._angle;
                let a = 1;
            } else {
                this._target.setPosition(locStartPosition.x + x, locStartPosition.y + y);
            }
        }
    }
}
export var arcMove = function(duration, R, angle){
    return new ArcMove(duration, R, angle);
}

这里的_positionDelta和_previousPosition暂时没有用到。由于需要根据精灵的自身旋转角度和给定的半径得出圆心。随后从当前位置开始圆周运动并调整精灵的自身旋转角度。_angle指定精灵的圆周运动度数,一圈是360,具体运动度数可以任意设置。_target则是精灵节点,在initWithTarget中完成初始化。_initAngle记录的是初始时,从水平方向到精灵的旋转角度,即记录精灵在圆上的初始位置。_rotation记录精灵初始的自身旋转角度。
js中重写initWithDurationinitWithTarget方法是通过cc.ActionInterval.prototype.initWithXXX.call()的方式进行的。但ts似乎没有把接口暴露出来,因此这里采用了super["initWithXXX"]()的形式。
余下内容计算出圆心、初始圆上位置等等。在update函数中,需要注意dt,比如给定精灵的运动时间duration为3秒,dt的变化还是从0到1.

使用方法

在使用的时候,import 该类。
let action = arMove(3, 100, 180);
随后节点runAction即可。

2011-11-03 19:43:52 yanghuiliu 阅读数 34333

猴子原创, 欢迎转载,转载请在明显处注明! 谢谢。

原文地址http://blog.csdn.net/yanghuiliu/article/details/6933352

做项目遇到了很多动画效果,基本大多数使用action实现的,当然也可以自己在每一帧的时候自己去实现,原理基本一样的。

1、使用CCMoveTo左右快速抖动。

CCPoint pointL=pointBg;
CCPoint pointR=pointBg;
pointL.x-=3;
pointR.x+=3;
CCMoveTo* moveLeft=CCMoveTo::actionWithDuration(0.08, pointL);
CCMoveTo* moveRight=CCMoveTo::actionWithDuration(0.08, pointR);
CCFiniteTimeAction* action= CCSequence::actions(moveLeft,moveRight,NULL);
CCActionInterval* actionShake=CCRepeatForever::actionWithAction((CCActionInterval*)action);
skillBg->stopAllActions();
skillBg->runAction(reSkillAnim);
pointBg就是你需要左右抖动的sprite。

CCMoveTo就是移动到哪里去,参数是移动时间,目的坐标点。(CCMoveBy是在当前位置移动多少距离,有点区别。)

CCSequence前面就说了,你可以看作为执行一个action的队列,遇到NULL就结束了。

因为是要实现不同的左右移动,所以我将左右移动包装了一下,包装成一个CCRepeatForever的action,它是不停的重复播放这个action。


2、CCJumpTo从一个地方跳跃到另外一个地方。

CCJumpTo* mJumpTo = CCJumpTo::actionWithDuration(0.2f, mToPosition, 30.0f, 1);
第一个参数是跳远的时间,第二个参数是跳跃的目的点,第三个参数是跳跃的最大高度,第四个参数是跳跃的次数。

3、CCDelayTime 可以用来作为暂停停多长时间。、

CCDelayTime *waiting=CCDelayTime::actionWithDuration(0.2f);

暂停0.2秒。


4、CCScaleTo 缩小/放大到。(区别CCScaleBy,缩小/放大多少)

CCScaleTo* scale=CCScaleTo::actionWithDuration(0.5, 0.2);
第一个参数时间,第二次参数缩放倍数,=1正常大小,>1放大,<1缩小


5、CCRotateTo旋转角度。和上面一样。角度0-360。


6、CCWaves 和CCShaky3D

CCActionInterval* waves = CCWaves::actionWithWaves(5, 20, true, false, ccg(15,10), 5);
CCActionInterval* shaky = CCShaky3D::actionWithRange(4, false, ccg(15,10), 5);

CCWaves波浪效果,CCShaky3D震动效果。它是全屏执行,具体效果,自己跑下就看到了,截图看不到什么效果。



2016-01-23 11:12:03 mydo 阅读数 1584

大熊猫猪·侯佩原创或翻译作品.欢迎转载,转载请注明出处.
如果觉得写的不好请多提意见,如果觉得不错请多多支持点赞.谢谢! hopy ;)


上回说到解决办法是使用CCTargetedAction类.

CCTargetedAction是一个很神奇的类,它本身没有什么动作效果,但是它可以把一个动作和一个特定目标绑定起来,并且返回一个新的Action,我们就可以按需要操作这个新的动作了.

不过该类在Cocos2d-x中和Cocos2d-2.x中才存在,而本猫使用的Cocos2d的版本为最新的3.4.9.而在cocos2d 的v3版本中,CCTargetedAction类是不存在的.

不过好消息是,2.x版本中的对应类是可以在3.x中使用的,我们只需要将原来的代码拷贝到3.x中就可以了.

但是光拷贝是不行的,据说会将一个动作执行2遍,这时我们必须手动再加一条代码:

-(BOOL)isDone{
    return [_action isDone];
}

即在具体类中写一个完成方法的重载即可.

好了,在Cocos2d v3.4.9中的准备工作都已完成,下面是实现代码:

NSMutableArray *actions = [NSMutableArray array];

    for (BattleUnitRoot *unit in sortBattleUnits) {
        BattleUnitRoot *target = _attackTargets[unit.playerData.battleIndex];

        CCTargetedAction *atkAction = [CCTargetedAction actionWithTarget:unit.sprite action:
                                                (CCActionFiniteTime*)[unit getAttackAction]];
        CCTargetedAction *atkedAction = [CCTargetedAction actionWithTarget:target.sprite action:
                                                (CCActionFiniteTime*)[target getAttackedAction]];
        [actions addObject:atkAction];
        [actions addObject:atkedAction];
    }

    CCActionSequence *seq = [CCActionSequence actionWithArray:actions];
    [self runAction:seq];

编译运行,以下是实际游戏到效果动画:

这里写图片描述

2015-03-25 16:57:07 djvc 阅读数 1110

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





2015-05-26 19:39:50 github_28498881 阅读数 1654

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