いぬでもわかる

iOSやCocos2dxが好きでときどきAndroid。できるだけ毎日何かしら書く事を目標に頑張る。

cocos2d-xのアクションを仕組み

Easingよく利用しますよね?
ただし、Easingを含んだ連続したアニメーションをしたい場合って
アクションとアクションのつなぎ目がスムーズにいかないですよね?
僕はそんな状況に直面しました。
具体的には以下のようなコードで、ユーザ画像をY軸に10移動させた後、背景画像を-10移動させようとしています。

Sequence* sequence = Sequence::create(MoveBy::create(1, Point(0, 10)), 
CallFunc::create([&](){
bg->runAction(EaseOut::create(MoveBy::create(1, Point(0, -10)), 3));
}),
,NULL);

user->runAction(sequence);

背景が動く事で、ユーザ画像はY軸に10しか動いていないのにY軸に20移動したように見えますよね。
しかしこれ動かしてみるとわかるのですが困る事が一点。
背景画像をEaseOutさせているため、ユーザの終速と背景画像の初速が異なってしまい、アクションの切り替えがスムージに見えません。

背景画像の初速を計算して、それをユーザ画像の動作の終速に設定しようかとも考えたんですが、背景画像の初速の初速がわからない!
というのも、EaseOut::createの第2引数がEasingの変化の割合を示しているのはわかるのですが、
その値がどのように実際のEasingに変化を与えるのかがわからなかったのです。

しょうがないので、ソースを読んで少しだけ理解してみました。
大体確認したソースは以下。
ActionInterval
ActionManager
EaseOut

特定のNodeにアクションを実行させるメソッドにrunActionがあります。
このrunAction実際に呼び出されると、第一引数で渡されたActionインスタンスは、ActionManagerのインスタンスにrunActionを実行したインスタンスとともに追加、保持されます。

ここで登場したActionManagerはすべてのアクションを管理する、シングルトンクラスです。
ActionManagerクラスにはupdateメソッドが実装されており、毎フレームupdateメソッドが呼び出されます。
updateメソッド内では、ActionManagerに追加されたすべてのActionを走査し、Actionが実行停止状態でなければ、Actionのstep関数をフレーム間の秒数を引数にして呼び出します。
ActionIntervalで定義されたstep関数は、これまでのアニメーション時間をアニメーション全体の時間で割ったアニメーションの進行割合を引数にしてupdateメソッドを呼び出します。

void ActionInterval::step(float dt)
{
    if (_firstTick)
    {
        _firstTick = false;
        _elapsed = 0;
    }
    else
    {
        _elapsed += dt;
    }
    
    this->update(MAX (0,                                  // needed for rewind. elapsed could be negative
                      MIN(1, _elapsed /
                          MAX(_duration, FLT_EPSILON)   // division by 0
                          )
                      )
                 );
}

updateメソッドは、MoveByやMoveToなどのアクションクラスでそれぞれオーバーライドされています。
実際の位置の変更などのアクションは、このupdateメソッドで行われています。
updateメソッド引数にはこれまでのアニメーションに要した時間をアニメーション全体時間で割った、アニメーションの進行割合が設定されています。
以下はMoveByのupdateメソッドです。進行割合とアニメーションpositionDeltaを掛け合わせた物を最初のポジションに足し合わせて、そのポジションをtargetとなるインスタンスに設定しています。

void MoveBy::update(float t)
{
    if (_target)
    {
#if CC_ENABLE_STACKABLE_ACTIONS
        Point currentPos = _target->getPosition();
        Point diff = currentPos - _previousPosition;
        _startPosition = _startPosition + diff;
        Point newPos =  _startPosition + (_positionDelta * t);
        _target->setPosition(newPos);
        _previousPosition = newPos;
#else
        _target->setPosition(ccpAdd( _startPosition, ccpMult(_positionDelta, t)));
#endif // CC_ENABLE_STACKABLE_ACTIONS
    }
}

ここまでで、大体どういった仕組みでアニメーションされているのか理解できましたが。
では、本題であるEasingをする際にはどういった動きになっているのか。
まず、クラスの継承関係から見ていきます。
EaseOutクラスは、EaseRateActionを継承し、EaseRateActionはActionRaseを継承し、ActionRaseがActionIntervalを継承しています。
次に、上記のクラスでここまででアクションに関係すると考えられる、setpとupdateメソッドについてオーバーライドの状況を追っていきます。

ActionEase
updateのオーバーライドをしている。
ActionEase::update(float time)
インスタンスを作るときに渡された第一引数のアクションのupdateメソッドを呼び出す。
EaseRateAction
step、updateのオーバーライドはしていない。
EaseOut
updateメソッドのオーバーライドをしている。

上記のようにstepはオーバーライドされていないのでEaseOutのupdateだけ見れば良さそうです。

void EaseOut::update(float time)
{
    _inner->update(tweenfunc::easeOut(time, _rate));
}

EaseOutに渡したアクションのupdateメソッドにtweenfunc::easeOut(time, _rate)を渡しています。
tweenfunc::easeOutは何をやっているか見てみるとtimeを1/rate乗したものを返しています。

float easeOut(float time, float rate)
{
    return powf(time, 1 / rate);
}

ここで帰ってきた値使ってMoveByなどのupdateメソッドを呼び出しているようですね!