CCMoveTo, CCMoveByなどのCCAction系の競合対策

CCMoveToなどのアクションでは、到達時間と目標値(By系は変更量)を指定します。

これらは、あるアクションをやっている途中にもうひとつ別のアクションを重ねると、思わぬ動作をすることがあります。
一つ例を挙げます。
あるCCSpriteオブジェクトが(0,0)にあるとします。
CCMoveToで座標(100,100)に1秒で移動するようにしたとします。
1秒が終わる前に、もうひとつ同じ内容のCCMoveToを実行したら、最終的には(100,100)には着きません。
もっと先に進んでます。
どこ辺りにつくかは、2番目のCCMoveToを起動したタイミング次第です。

これがなぜ起こるか説明します。
CCMoveToなどのCCAction系は、runActionで起動すると、毎フレームどれだけ移動すればいいか計算し、
対象のCCNode系オブジェクトの毎フレームのループで、変更量を積み重ねます。
CCMoveToで言えば、setPositionによってCCNodeの位置を毎フレーム変更します。
2つのCCMoveToが同時に走ると、positionプロパティに、毎フレーム、別の値がプラスされることになります。
当然、目的の場所には着きません。

このような、ひとつのリソースや変数(ここで言えばposotionプロパティ)に2つのものから書き込みがされることを、情報系の用語では「競合」と言います。

今回は、僕が競合による期待しない動作を回避するために対策した方法をいくつか紹介します。



①競合が起こるようなアクションを同時に2つ 走らせない
そもそも競合を起こさない、という話です。
大体、CCRotationとCCMoveToのような別種のものを2つ重ねるケースはあるでしょうが、同種のものを重ねなければならないケースなど滅多にないでしょう。
最初から競合が絶対に発生しないようなソースを書くのが一番です。

それができない場合は、1つ目が終わってから2つ目のものを起動するか、2つ目のものを起動する直前に1つ目のものを停止させればOKです。

前者はCCCallFuncなどのコールバックで実現出来ますね。
後者のための一番簡単なメソッドはstopAllActions()です。
全て止めるのが無理なら、一つ目のアクションにsetTag(int タグ値)でタグをセットしておいて、stopActionByTag(タグ値)で止めてやりましょう。


②ccConfig.hのCC_ENABLE_STACKABLE_ACTIONSマクロを0に
実は、CCMoveToなどのpositionを変えるアクションが2つ重なった時に、ちゃんと目的地で止めるか、それとも2つ分の変更値を合算するようにするか、というのは、コンフィグマクロで指定出来ます。
それがccConfig.hのCC_ENABLE_STACKABLE_ACTIONSです。
バージョン2.1以降は2つ分の変更値が合算されるようになっているらしいです。
実際は合算させねばならないようなケースはあまりないでしょう。
基本的に0にして使って問題ないと思います。


③番外編、ccTouchMoveメソッド内でアクションを起動する処理を書かない
あまり気を使わずにゲームを作っていると、ccTouchMoveメソッド内でCCMoveTo系のアクションを起動するコードを書いちゃうこともあるかと思います。
ただ、ccTouch系のタッチデリゲートメソッドは、現在のゲームのループに割り込んで呼ばれます。
cocos2d-xは、毎フレーム、update系のループメソッドを実行することで動いているわけですが、ccTouch系のメソッド呼び出しは、それに対して割り込んできます。
ccTouchMoveメソッドの中でCCMoveToを起動していたりしたら、当然競合が発生します。


ていうか、1フレームの間に何度も呼ばれるメソッドでそんな処理するのはCPU負荷的に無駄です。
(2013-11-14追記:人に聞いた情報ですが、上記は誤りのようです。ccTouchMovedの頻度は、そんなに高くないらしいです。頻度はデバイスに依存し、測定した方によるとiOSで4~5回/秒、Androidで頻度が高い端末だと12回/秒くらいだったそうです。cocos2d-x界隈で有名な方に聞いた情報ですが、製品開発のときは必要であれば調査するのをおすすめします。)

ゲームの開発では、ユーザからの入力もフレーム単位でしか扱わないのが通常だと思います。
cocos2d-xでそれに従うなら次のようにするといいとおもいます。

1,TouchContainerクラスを用意する

class TouchContainer {
public:
CCTouch* touchBegan;
CCTouch* touchMove;
CCTouch* touchEnd;
CCTouch* touchCanceled;
};

2,ccTouch系のメソッドでは上記のクラスに引数のCCTouchの値をコピーするだけにする。

3,updateメソッドなどのフレーム処理で、上記の値を使って、タッチに対する処理を行うようにする。

これで、通常のゲーム開発と同じやり方でユーザ入力が扱えると思います。


今回の記事は以上です。