夏までにiPhone アプリつくってみっか!

趣味でiPhone/Androidアプリを開発し、日々勉強した事を書いています。オープンワールド系レースゲームをUnityで開発中です。

【cocos2d】ぎこちないCCActionの動きを滑らかにしてみた Part 2

前回のPart 1に掲載したSteering BehaviorのArrivalアルゴリズムのプログラムについて解説したいと思います。自分の理解を深めるために絵を描きましたのでプログラムの各部をとそれに対応する絵を使って説明します。説明の中で、速度、加速度、位置など単位が異なる値をそのまま足したり引いたりしていることが気になるかも知れませんが、時間の単位は全て1フレーム(1/60秒)だとお考えください。つまり、時間は常に1なので、計算上は数値をそのまま足しても問題ないのです。例えば、東京から大阪まで向かう車があったとして、東京から100kmの地点を100km/hの速度で走っている場合、常にその速度をキープしていれば1時間後の車の位置は100km+100km/h * 1時間 = 200km、東京から200kmの地点にいるはずです。このように、実際には計三時に時間の値「1」を掛けて単位を合わせているとお考えください。

まず、イニシャライザとインスタンス変数について簡単に説明します。イニシャライザの引数を順番に見て行くとtargetは追いかける目標のオブジェクトです。frameは表示するスプライト、maxAccelは最大加速度です。これを大きくすると加速、減速、旋回性能などが良くなりますが、大きくしすぎると目標にピッタリ重なるように動くので肝心の滑らかさは損なわれます。maxSpeedはその名の通り最大速度です。brakeDistanceは目標にどれくらい近づいたら減速を開始するかを設定するパラメーターです。これらは全てインスタンス変数にコピーしておきます。self.radius, self.tagは当たり判定に使う情報です。スプライトの初期位置はターゲットの位置に合わせます。

肝心のArrivalアルゴリズムはupdateメソッドに含まれているので、フレーム毎に呼ばれ、その度に次のフレームのためのキャラクターの位置を計算します。キャラクターの位置と速度は前のフレームの情報を引きずってきますので、それを図にします。

f:id:takujidev:20130418222646p:plain

ピンクが自分のキャラクターでオレンジは目的の位置を示すキャラクターです。ピンクがオレンジを追いかける動きになります。前のフレームでの計算の結果、自分はこの位置に速度_velocityで移動してきました。ターゲット(_target:オレンジ)も同様に前のフレームではどこか別の場所にいて、今この場所にいます。自分(self:ピンク)次のフレームでなんとかターゲットに追いつこうとしています。

f:id:takujidev:20130418222651p:plain

CGPoint desiredVelocity = ccpSub(_target.position, self.position);

目標の位置から自分の現在位置をccpSubで引くことで現在位置から目標の位置へのベクトルdesiredVelocityを求めます。目標が動かないと仮定すると、1フレームあたりdesiredVelocityの速度で移動すれば次のフレームには目標地点に到着する計算です。

float distance = ccpLength(desiredVelocity);

desiredVelocityベクトルの長さを求めて変数distanceに入れます。これは現在位置から目標の位置の距離ですが、速度のベクトルdesiredVelocityの大きさ=速さでもあります。ここでは距離として値を使用しますので変数名をdistanceにしました。

f:id:takujidev:20130418222656p:plain

float speed = 0;

速さを入れる変数speedを作り、初期化します。次のif〜else文で必ず値が入るので初期化は不要ですが、念のためです。

if (distance < _brakeDistance) {
    speed = _maxSpeed * distance / _brakeDistance;
} else {
    speed = _maxSpeed;
}

現在位置から目標への距離が_brakeDistanceより小さければ距離に応じて速度を調節します。距離が_brakeDistanceであればはやさは _maxSpeed, 距離が0であればはやさは0, 距離が_brakeDistanceの半分であれば速さは_maxSpeedの半分になります。
距離が_bakeDistance以上の場合は速度は_maxSpeedになります。

if (speed == 0) {
    return; 
}

ここはArivalのアルゴリズムには含まれていませんが、私がハマったところです。距離が0であれば残りの計算をせずに処理を終了します。この判定を入れないとバグってスプライトが表示されません。デバッガで調べてみるとベクトルの値がnanになっていました。nanはnot a numberの略だそうです。それ以上詳しくは見ませんでしたがおそらく次の行のccpNormalize()の内部で0での割り算が発生しているのだと思います。詳しくは後述します。

f:id:takujidev:20130418222707p:plain

desiredVelocity = ccpMult(ccpNormalize(desiredVelocity), speed);

ccpNormalizeは単位ベクトルを求める関数です。単位ベクトルとは大きさが1のベクトルです。ccpNormalizeをすることにより、ベクトルの向きをそのままに大きさを1にして、それにccpMultで大きさを掛けることでベクトルの向きをそのままに、長さを任意に設定できます。上の行では速度ベクトルdesiredVelocityの大きさ=速さをspeedに設定しています。
先ほどのnanの話に戻ると、単位ベクトルはベクトルのx, yの各成分をベクトルの大きさで割ることで求められますが、ベクトルの大きさが0のときは0での割り算となり、解なし=nanになっているのだと思います。

NSAssert((_maxAccel > 0.0) && ( _maxSpeed > 0.0), @"_maxAccel and _maxSpeed should be greater than 0");

この行は念のため入れています。先ほどと同じ理由でccpNormalizeで0での割り算が行われる可能性があるからです。
NSAssert関数は、最初の引数で「こうなっているべきだ」という条件を入れます。そうでない場合第2引数で指定するメッセージを表示し、プログラムがエラー終了します。上記の場合は_maxAccelと_maxSpeedのどちらか、あるいは両方がゼロより大きくないとプログラムが終了します。NSAssertで引っかかったときに何が起こるのか試してみました。このようなエラーログがコンソールに吐かれ、プログラムが止まりました。クラス名、メソッド名、ファイル名、行番号と指定したメッセージが表示されているのがわかると思います。なお、NSAssertはデバッグ用であり配布用のプログロムでは取り除かれます。

2013-04-17 23:38:34.993 TestProject[3268:c07] *** Assertion failure in -[MySteering update:], /Users/Test/MySteering.m:57
2013-04-17 23:38:34.995 TestProject[3268:c07] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: '_maxAccel and _maxSpeed should be greater than 0'

話をArrivalアルゴリズムに戻します。

f:id:takujidev:20130418222713p:plain

CGPoint steerAccel = ccpSub(desiredVelocity, _velocity);

_desiredVelocityは望むべき速度です。ccpSubで_desiredVelocityから現在の速度_velocityを引いたベクトルをsteerAccelに入れます。steerAccelは速度の変化を表すベクトルなので加速度ベクトルとなります。ちなみに、ベクトルの大きさ(速さ)が変わらなくても向きを変えるには加速度が必要です。

f:id:takujidev:20130418222718p:plain

if (ccpLength(steerAccel) >= _maxAccel) {
    steerAccel = ccpMult(ccpNormalize(steerAccel), _maxAccel);
}

steerAccelの大きさが最大加速度_maxAccelより大きい場合は、steerAccelの大きさを_maxAccelに制限します。この制限された部分が滑らかな動きの要となります。_maxAccelを小さくするとより滑らかな動きになりますが、追従性が悪くなります。逆に大きくすると追従性が良くなりますが、動きのカドが残ります。

f:id:takujidev:20130418222723p:plain

_velocity = ccpAdd(_velocity, steerAccel);

今度は加速度を現在の速度に足す事で速度を変化させます。

f:id:takujidev:20130418222728p:plain

if (ccpLength(_velocity) >= _maxSpeed) {
    _velocity = ccpMult(ccpNormalize(_velocity), _maxSpeed);
}

計算の結果求められた速度の大きさが_maxSpeedを超える場合は_maxSpeedに制限します。_maxSpeedはキャラクターを動かしたい速度にだいたい合わせておけばいいと思います。

f:id:takujidev:20130418222734p:plain

self.position = ccpAdd(self.position, _velocity);
self.rotation = 90.0 + CC_RADIANS_TO_DEGREES(-ccpToAngle(_velocity));

最後に、現在の位置に速度を足して次のフレームの位置を求めます。self.positionはCCNodeのプロパティで、毎フレームの描画処理でスプライトを描画する位置を入れておきます。2行目はArrivalアルゴリズムとは関係ありませんが、キャラクターの頭を進行方向に向ける処理です。

例としてビデオを貼付けておきます。上の説明ではピンクのキャラクターがオレンジのキャラクターを追いかけていますが、このビデオでは明るい色のキャラクターが暗い同じ色のキャラクターを追いかけています。
暗いピンク色のキャラクターはCCActionのCCMoveByで四角形にうごいています。暗いオレンジ色のキャラクターはマウスでドラッグして動かしています。(ビデオはiPhoneシミュレーターをキャプチャーしたので。実機の場合はもちろんタッチしてドラッグします。)

次回Part3では、このMySteeringクラスを使う側の実装を解説したいと思います。