cocos2d-x v3.9 与ActionInterval的孩子们之间的对话(2)

原创 2015年11月21日 20:41:46

我:我看在ActionInterval的孩子中你们好像有些与众不同。
Sequence、ExtraAction、Repeat、RepeatForever、DelayTime以及ReverseTime(异口同声):没错,我们的能力不是作用于您指定的物体上,而是作用于您指定的动作上。换句话说,我们是为了辅助其他动作的运行而存在的。
我:这无私奉献的精神,那么来One By One的做个自我介绍吧。


Sequence:我能将您交给我的动作一个接着一个的执行,对了就是您刚才说的词,One By One。给我多少个动作我都能胜任,但是请注意一定要在最后一个动作参数之后加上一个nullptr参数,用这种方式来告诉我:“我给你的动作就这么多了,没有其他的了。”
我(不明觉厉):看起来很厉害的样子,说说怎么使用吧。
Sequence:好的。我可以接收任意多个动作作为参数,就像刚才描述中的用法,

sprite->runAction(Sequence::create(action1, action2, ..., nullptr));    // ...代表可以写任意多个动作。

还可以接收一个存储着多个动作的容器,

Vector<FiniteTimeAction *> vector;
vector.pushBack(static_cast<FiniteTimeAction *>(action1));
vector.pushBack(static_cast<FiniteTimeAction *>(action2));
...
sprite->runAction(Sequence::create(vector));

我:我看到你的第一种使用方式的函数声明最后有个CC_REQUIRES_NULL_TERMINATION,我研究了一下,请参见我的这篇博文
Sequence:没错,正如您所说的那样。
我:给你多少个动作你都能接收,你的肚子到底有多大?
Sequence::),其实我的肚子没有多大,再说我也不是用肚子存储的,是用我的双手,所以我只能存储两个动作。我之所以能存储多个动作,是因为我影分身出了多个同胞来帮助我,人多力量大嘛。我的左手拿着您给我的其中一个动作,右手就拿着我的一个影分身;我右手的影分身也和我一样,左手拿着您给我的其中一个动作,右手拿着我的另外一个影分身,以此类推;我的最后一个影分身左右手拿的就是您给我的最后两个动作。这么说可能有些绕,还是来张图更直观些。比如您指定给我5个动作,那么我就是按如下方式存储的,

// 第一个是我,后面是我影分身出的3个同胞。
Sequence -- Sequence -- Sequence -- Sequence -- action1
         |           |           |           |- action2
         |           |           |- action3
         |           |- action4
         |- action5

此外还要说明两点,
1、我向长辈们传递的“规定的时间”是我手里两个动作各自的“规定的时间”之和。还是上面的那个例子,比如action1~action5规定的时间是10~6,还是上面的那张图,在括号中表示向长辈们上报的规定的时间,

Sequence(40) -- Sequence(34) -- Sequence(27) -- Sequence(19) -- action1(10)
             |               |               |               |- action2(9)
             |               |               |- action3(8)
             |               |- action4(7)
             |- action5(6)

2、如果您只传递给我一个动作,那么我的另一只手也不会空着,我会自己创建一个ExtraAction类型的动作在手里攥着,这个动作的“规定的时间”为0(因为这个动作也是ActionInterval的儿子,并且他没有上报规定的时间,而长辈FiniteTimeAction在构造函数中初始化规定的时间为0)。

Sequence(10) -- action1(10)
             |- extraaction(0)

并且这个动作在update()的时候什么也不做,您可以理解为一个空动作。
我:为什么是这么个方式?
Sequence:这与我One By One的运行动作的实现方式有关,这个实现方式还是来看代码更直观些,首先runAction()时会调用我的startWithTarget(),

void Sequence::startWithTarget(Node *target)
{
    ActionInterval::startWithTarget(target);
    /* _split代表第一个动作与第二个动作在时间上的百分比分界线。
     * 比如我的_split = 34 / 40,我的第一个影分身的_split = 27 / 34,我的其他影分身的_split以此类推。
     * _last代表上一次update()后执行的是第几个动作,这个值会在update()中赋值。
     */
    _split = _actions[0]->getDuration() / _duration;
    _last = -1;    // 现在还没有执行过动作,所以初始化为-1。
}

接下来动作运行起来后,ActionManager会调用我的update(),

void Sequence::update(float t)
{
    int found = 0;    // 本次需要执行第几个动作,初始化为需要执行第一个动作。
    /* 因为Sequence上报了两个动作的规定时间和,所以t是这个时间和的时间进度百分比。
     * new_t的作用是存储针对于每个动作(_actions[0]或_actions[1])的时间进度百分比。
     */
    float new_t = 0.0f;

    if( t < _split ) {    // 需要执行第一个动作。
        found = 0;
        /* 如果_actions[0]上报的规定的时间就为0时,_split就等于0。
         * 否则_split就是个时间上的百分比分界线。
         */
        if( _split != 0 )
            new_t = t / _split;    // _actions[0]的时间进度。
        else
            new_t = 1;    // 对于没有实质的动作,进度直接是100%就好了。

    } else {    // 需要执行第二个动作。
        found = 1;
        /* 如果_actions[1]为ExtraAction或者上报的规定的时间就为0时,_split就等于1。
         * 否则_split就是个时间上的百分比分界线。
         */
        if ( _split == 1 )
            new_t = 1;    // 对于没有实质的动作,进度直接是100%就好了。
        else    // 如果_actions[1]非ExtraAction。
            new_t = (t-_split) / (1 - _split );    // _actions[1]的时间进度。
    }

    if ( found==1 ) {

        if( _last == -1 ) {
            // _actions[0]被跳过了(有可能在别的代码部分运行时间过长),需要直接执行完它。
            _actions[0]->startWithTarget(_target);
            if (!(sendUpdateEventToScript(1.0f, _actions[0])))
                _actions[0]->update(1.0f);
            _actions[0]->stop();
        }
        else if( _last == 0 )
        {
            // _actions[0] --> _actions[1]。
            if (!(sendUpdateEventToScript(1.0f, _actions[0])))
                _actions[0]->update(1.0f);
            _actions[0]->stop();
        }
    }
    else if(found==0 && _last==1 )    // 现在需要执行_actions[0],但上一次执行的是_actions[1],这种情况应该不会出现。
    {
        // Reverse mode ?
        // FIXME: Bug. this case doesn't contemplate when _last==-1, found=0 and in "reverse mode"
        // since it will require a hack to know if an action is on reverse mode or not.
        // "step" should be overridden, and the "reverseMode" value propagated to inner Sequences.
        if (!(sendUpdateEventToScript(0, _actions[1])))
            _actions[1]->update(0);    // _actions[1]复位。
        _actions[1]->stop();    // _actions[1]停止执行。
    }

     * _actions[0]执行完成不会走到这里,_actions[1]执行完成会进入这里。
    if( found == _last && _actions[found]->isDone() )    // 如果一直在执行的动作已经执行完成了。
    {
        // _actions[0]执行完成不会走到这里,_actions[1]执行完成会进入这里。
        return;
    }

    if( found != _last )    // 需要切换动作(开始执行_actions[0]或者_actions[0] --> _actions[1])。
    {
        /* 着重看这里和下面的update()。
         * 如果这个动作是Sequence,那么同样会执行这个Sequence的这么个流程,
         * 联系上面多个动作如何上报规定的时间看,把他们联系在一起思考。
         * 如果你想通了,会发现这种调用方式有点儿像递归,
         * 但却不是通过不断的调用自己相同的函数而实现的,而是通过调用同胞的。
         */
        _actions[found]->startWithTarget(_target);
    }
    // 如果绑定了脚本,会将时间进度和动作交给脚本,由脚本来执行动作。
    if (!(sendUpdateEventToScript(new_t, _actions[found])))
        _actions[found]->update(new_t);    // 如果没有绑定脚本就会到这里来处理,执行动作。
    _last = found;    // 存储本次执行的是第几个动作。
}

我:这种实现方式很有意思,看起来也很聪明。
Sequence:谢谢夸奖! :)

我:下一个到谁了?
Repeat:到我了,我能将您指定的动作重复执行指定的次数。至于实现上,还是看源码来的直观些,首先创建我时我会上报规定的时间,

bool Repeat::initWithAction(FiniteTimeAction *action, unsigned int times)
{
    // 上报的规定的时间为:动作每次用时 × 执行次数。
    float d = action->getDuration() * times;

    if (ActionInterval::initWithDuration(d))
    {
        _times = times;
        _innerAction = action;
        action->retain();

        /* 这里的dynamic_cast有类型检查的作用,
         * 判断action是否与ActionInstant有继承关系,即action是否为瞬时动作类型。
         */
        _actionInstant = dynamic_cast<ActionInstant*>(action) ? true : false;
        //an instant action needs to be executed one time less in the update method since it uses startWithTarget to execute the action
        /* 上面的这段注释可能是历史遗留问题,
         * 因为现在所有的瞬时动作没有用startWithTarget()执行动作的,
         */
        if (_actionInstant)
        {
            _times -=1;    // 我认为Repeat::update()中的issue #1288就是这里导致的。
        }
        _total = 0;

        return true;
    }

    return false;
}

接着runAction()时会调用我的startWithTarget(),

void Repeat::startWithTarget(Node *target)
{
    _total = 0;
    _nextDt = _innerAction->getDuration()/_duration;    // 存储每次动作执行完成的时间进度百分比(这里初始化为第一次的)。
    ActionInterval::startWithTarget(target);    // Repeat父类的startWithTarget()。
    _innerAction->startWithTarget(target);    // 被执行动作的startWithTarget()。
}

最后动作运行起来后,ActionManager会调用我的update(),

void Repeat::update(float dt)
{
    if (dt >= _nextDt)    // 如果本次动作执行完成。
    {
        // 本次动作执行完成并且还未超出执行次数。
        while (dt > _nextDt && _total < _times)
        {
            // 如果绑定了脚本,会将时间进度和动作交给脚本,由脚本来执行动作。
            if (!(sendUpdateEventToScript(1.0f, _innerAction)))
                _innerAction->update(1.0f);    // 让本次动作update()完成。
            _total++;    // 执行次数+1。

            _innerAction->stop();    // 本次动作结束。
            _innerAction->startWithTarget(_target);    // 重新开始。
            _nextDt = _innerAction->getDuration()/_duration * (_total+1);    // 下一次动作执行完成的时间进度百分比。
        }

        // fix for issue #1288, incorrect end value of repeat
        // 非瞬时动作不会有这个问题,瞬时动作才会有,罪魁祸首看Repeat::initWithAction()中。
        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)    // 如果整个Repeat执行完成。
            {
                /* 这里这么做是因为上面的while()在整个Repeat执行完成时没有做判断,
                 * 又将动作重新启动了,所以这里要停止。
                 */
                if (!(sendUpdateEventToScript(1, _innerAction)))
                    _innerAction->update(1);
                _innerAction->stop();
            }
            else    // 如果整个Repeat还未执行完成。
            {
                // issue #390 prevent jerk, use right update
                /* 这里和下面的fmodf(dt * _times,1.0f)是一个作用,只不过是另一种实现方式。
                 */
                if (!(sendUpdateEventToScript(dt - (_nextDt - _innerAction->getDuration()/_duration)
, _innerAction)))
                    _innerAction->update(dt - (_nextDt - _innerAction->getDuration()/_duration));
            }
        }
    }
    else    // 如果本次动作还没有执行完成。
    {
        // 如果绑定了脚本,会将时间进度和动作交给脚本,由脚本来执行动作。
        if (!(sendUpdateEventToScript(fmodf(dt * _times,1.0f), _innerAction)))
            /* dt = 当前流逝时间 / (动作的_duration × _times),看Repeat上报的时间就是这个,
             * 所以dt × _times = 当前流逝时间 / 动作的_duration,
             * 所以fmodf(dt * _times,1.0f)就是针对于每次动作的时间进度百分比。
             */
            _innerAction->update(fmodf(dt * _times,1.0f));    // 继续执行动作。
    }
}

我:实现的好像有些臃肿,其实我觉得可以更简单的,比如,

bool Repeat::initWithAction(FiniteTimeAction *action, unsigned int times)
{
    float d = action->getDuration() * times;

    if (ActionInterval::initWithDuration(d))
    {
        _times = times;
        _innerAction = action;
        action->retain();
        _total = 0;

        return true;
    }

    return false;
}

void Repeat::update(float dt)
{
    if (dt >= _nextDt)
    {
        while (dt > _nextDt && _total < _times)
        {
            if (!(sendUpdateEventToScript(1.0f, _innerAction)))
                _innerAction->update(1.0f);

            _innerAction->stop();
            if(++_total < _times)
            {
                _innerAction->startWithTarget(_target);
                _nextDt = _innerAction->getDuration()/_duration * (_total+1);
            }
        }
    }
    else
    {
        if (!(sendUpdateEventToScript(fmodf(dt * _times,1.0f), _innerAction)))
            _innerAction->update(fmodf(dt * _times,1.0f));
    }
}

Repeat:嗯,好的,希望设计者能看到。
我:我看到了个和你长得很像的家伙。
RepeatForever:您是在说我吗?我的确和Repeat的功能类似,我可以让您指定的动作永远重复运行下去。
我:哦?说来听听。
RepeatForever:我的实现与Repeat不同,我不上报规定的时间,而是重写了老爸的step()由我自己来管理时间,

void RepeatForever::step(float dt)    // 注意这里传入的是从上一帧到这一帧流逝的时间。
{
    /* 让具体动作的老爸管理他的孩子的时间。
     * 本次动作未执行完成,ActionManager不停的调用RepeatForever::step(),
     * RepeatForever::step()不停的调用具体动作的step()。
     */
    _innerAction->step(dt);
    if (_innerAction->isDone())    // 本次动作执行完成。
        /* 以下对于diff的计算以及使用均是为了防止抖动(jerk)。
         * 但具体抖动是什么样的我没有研究过。
         */
         // 动作刚刚执行完成时getElapsed()会比getDuration()大一点点,isDone()就是据此判断动作是否执行完成的。
        float diff = _innerAction->getElapsed() - _innerAction->getDuration();
        if (diff > _innerAction->getDuration())    // 如果跳过了多次动作(有可能是在别的地方执行时间过长)。
            diff = fmodf(diff, _innerAction->getDuration());    // 忽略跳过的动作。
        _innerAction->startWithTarget(_target);    // 动作重新开始。
        // to prevent jerk. issue #390, 1247
        // 由于是永远重复执行,所以两次动作的衔接要连贯,diff这点时间也要step()一下。
        _innerAction->step(0.0f);
        _innerAction->step(diff);
    }
}

我:实现的层面不同,不过我觉得你俩既然功能类似,还是应该合并在一起,实现上往相同的模式转化更合理。

我:还有两位,不要害羞,也来说两句吧。
DelayTime:到Zzz我了Zzz,我的Zzz作Zzz用Zzz是睡觉。您Zzz让我Zzz睡多久Zzz我Zzz就睡Zzz多Zzz久Zzzzzz(进入深度睡眠…)。
我:看出来了…,我说你现在先不要睡了…
DelayTime:好的,您给我规定的睡眠时间是以秒为单位的。我在实现上也非常的简单,总之一句话,就是什么都不做。您告诉我的规定的时间直接交给老爸,update()中也什么都不做。
我:好了…,你可以继续睡了。
DelayTime:好的。(瞬间进入深度睡眠中…)
我:最后就剩你了,ReverseTime。
ReverseTime:来了。我的作用有点儿像时光机,能让时光倒流。举个例子,

// 假设sprite的初始位置为(10, 10)。
auto moveby = MoveBy::create(3, Vec2(100, 100));
/* 本来moveby是让sprite在3秒内从(10, 10)移动到(110, 110),
 * 而经过我手之后,sprite将会在3秒内从(100, 100)移动到(10, 10)。
 * 这里可以对比moveby->reverse(),效果是不一样的哦。
 */
sprite->runAction(ReverseTime::create(moveby));

而我的实现上也很简单,创建一个您提供的动作的分身,然后传递给他总时间进度与当前时间进度百分比的差值,

_other->update(1 - time);

不过为什么要创建个动作的分身我也没有搞明白,嘿嘿。
我:你俩的实现倒都是很简单。今天聊了很多,先暂时到这里。

版权声明:本文为博主原创文章,未经博主允许不得转载。

cocos2d-x v3.9 与ActionInterval的孩子们之间的对话(1)

我(对MoveBy):你的爹地ActionInterval除了你还有其他的孩子吗? MoveBy:有,而且还有很多。他们也几乎都是一对儿一对儿的,不是父子就是兄弟,当然也有独生子。 我(窃窃私语)...

cocos2d-x v3.9 关于RotateTo和SkewTo的疑问

最近在研究cocos2d-x v3.9的RotateTo时遇到了个问题,不知道是否算是Bug。首先说一下我理解的RotateTo。RotateTo不关心精灵当前的角度,将精灵旋转至指定的角度。无论指定...

cocos2dx v3.9 与SpriteFrameCache和AnimationCache之间的对话

我:前段时间去过动画制作组,你们两个的名字与动画制作组中的SpriteFrame和Animation很相似,难道说你们之间有什么关系? SpriteFrameCache和AnimationCache...

使用python编译Cocos2d-x3.9的cpp-tests文件夹下的所有项目

一次偶然浏览cocos论坛时,得知了引擎中提供了许多例子。然后上网搜索了一下是否能进行编译运行,结果还真的能。特此记录一下编译并运行例子的流程。 1.配置好Python语言以及ant的路径。(由于我...

cocos2d-x 3.9入门

这周刚刚开始接触cocos2d-x,第一次打开helloword的就懵了不知道该干嘛。所以这里记录一下方便日后查找。 #include "main.h" #include "AppDelegate....

win10系统搭建vs2015+cocos2d-x 3.9开发环境

前段时间卸掉了win7重装了win10系统,导致以前的vs2013+coccos3.2的开发环境不可用了,所以重新装了vs2015+cocos3.9的开发环境,安装过程中遇到了比较多的坑。 一、安装...
  • chenxu6
  • chenxu6
  • 2015年12月27日 22:44
  • 8778

Cocos2d-x 3.9教程:4.精灵的创建和基本操作

1. 精灵的创建和基本操作 1.1. 精灵的创建 ①正常创建 Sprite *sprite = Sprite::create("man.png");//通过图片创建精灵 sprite->setPos...

Cocos2d-x 3.9教程:10.使用CocosStudio的UI编辑器从UI文件中加载布局和控件

Cocos2d-x 3.9教程 10. 使用CocosStudio的UI编辑器从UI文件中加载布局和控件 1.1. 使用CocosStudio的UI编辑器 1.1.1. 安装和启动 从...

Win10如何安装vs2015+cocos2d-x 3.9开发环境?安装配置教程分享

Win10如何安装vs2015+cocos2d-x 3.9开发环境?安装配置教程分享 http://www.xitonghe.com/jiaocheng/Windows10-5631.html 前...
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:cocos2d-x v3.9 与ActionInterval的孩子们之间的对话(2)
举报原因:
原因补充:

(最多只允许输入30个字)