読者です 読者をやめる 読者になる 読者になる

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

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

【偽スペースハリアー】ベジェ曲線で滑らかに敵を動かしてみた

cocos2d スペースハリアー TravelShooting JP

これまでは岩とか木とか動かない敵ばかりでしたが、いよいよまともに動く敵の登場です。
3D空間を動かすとなると残念ながらx, y座標限定のcocos2dのCCActionは役に立ちません。
3D空間を動かす方法はいろいろ考えられますが、スペハリのほとんどの雑魚キャラは決められたルートを動くだけなので、あらかじめフレーム毎の座標を記録して、それを毎回update:メソッドで読み出すのが一番軽い処理になると思います。
軽い分データ量はかさみますが、x,y,z座標各4バイトで1フレームあたり12バイト。10秒のモーションだと600フレームなので7200バイト。100モーション作っても1Mバイトのメモリーでおつりがたくさん来ます。
64ビット環境だと倍になるかも知れませんが、それでもたいした事はありません。
アプリのサイズに関しても心配する事はありません。実際にデータとして持つのは1モーションあたり数点から数十点の座標のみで、そこからアプリ内でフレーム毎の座標を生成します。

さて、肝心の敵キャラの動きについてですが、カクカクした動きはみっともないのでスルッと滑らかに動かしたいですね。しかし、数点から数十点のデータだとカクカク動いてしまいそうです。
BeeClusterのときは、CCActionでターゲットをカクカク動かし、それをキャラクターが滑らかに追いかけるような処理を実装しました。

【cocos2d】ぎこちないCCActionの動きを滑らかにしてみた - 夏までにiPhone アプリつくってみっか!
【cocos2d】ぎこちないCCActionの動きを滑らかにしてみた Part 2 - 夏までにiPhone アプリつくってみっか!
【cocos2d】ぎこちないCCActionの動きを滑らかにしてみた Part 3 - 夏までにiPhone アプリつくってみっか!

今回は別の方法を使ってみました。
カクカクした座標データから滑らかな軌跡データを生成するのにベジェ曲線による補間を行っています。今回使った二次ベジェ曲線による補間処理はそれほど重くはなさそうので恐らくリアルタイム処理もできると思います。

まずは、今回の成果を動画でご確認下さい。

いつもより短くあっさり終わってしまいますが、ご勘弁を。

ベジェ曲線というと難しそうですが、考え方自体はそんなに難しくはありません。
詳しくはウィキペディアを見てください。
途中の難しい式は飛ばして、下の方の動く図を見るとわかりやすいです。
p0, p1, p2の3点があり、p0からp2へ例えば100フレーム掛けて移動する場合、30フレーム目の位置は次のように求められます。

p3 = p0からp1へ向かうベクトル * 30/100 + p0の座標
(つまり、p0とp1を結ぶ直線の、p0を起点として30%の位置)
p4 = p1からp2へ向かうベクトル * 30/100 + p1の座標
p5 = p3からp4へ向かうベクトル * 30/100 + p3の座標 

p5がベジェ曲線上の30フレーム目の位置になります。

p0からp1へ向かうベクトル * 30/100 + p0の計算は、

(p1 - p0) * 0.3 + p0

で求めます。
ベクトルの演算なので、x, y, zの各成分を足したり引いたり掛けたりします。

上の式を実装するとこのようになります。
p4とかp5は違う名前になっていますがわかるでしょうか。
また、このプログラムでは呼び出し側で上で言うところの30/100を計算した値をtに入れて使うようになっています。Point3Dというのはx, y, zをメンバーに持つ構造体です。

- (Point3D)linearInterpolateP0:(Point3D)p0 P1:(Point3D)p1 t:(CGFloat)t
{
    Point3D p0ToP1AtT;
    Point3D vectorP0ToP1 = [self pathPointSub:p0 from:p1];
    vectorP0ToP1 = [self pathPointMult:vectorP0ToP1 by:t];
    p0ToP1AtT = [self pathPointAdd:p0 and:vectorP0ToP1];
    return p0ToP1AtT;
}

- (Point3D)bezierInterpolateP0:(Point3D)p0 P1:(Point3D)p1 P2:(Point3D)p2 t:(CGFloat)t
{
    Point3D p0ToP1AtT = [self linearInterpolateP0:p0 P1:p1 t:t];
    Point3D p1ToP2AtT = [self linearInterpolateP0:p1 P1:p2 t:t];
    Point3D pBezier = [self linearInterpolateP0:p0ToP1AtT P1:p1ToP2AtT t:t];
    return pBezier;
}

- (Point3D)pathPointAdd:(Point3D)p0 and:(Point3D)p1
{
    Point3D pAdd;
    pAdd.x = p0.x + p1.x;
    pAdd.y = p0.y + p1.y;
    pAdd.z = p0.z + p1.z;
    return pAdd;
}

- (Point3D)pathPointSub:(Point3D)p0 from:(Point3D)p1
{
    Point3D pSub;
    pSub.x = p1.x - p0.x;
    pSub.y = p1.y - p0.y;
    pSub.z = p1.z - p0.z;
    return pSub;
}

- (Point3D)pathPointMult:(Point3D)p0 by:(CGFloat)t
{
    Point3D pMult;
    pMult.x = p0.x * t;
    pMult.y = p0.y * t;
    pMult.z = p0.z * t;
    return pMult;
}

なお、ただの連結した直線からはベジェ曲線を生成できないので、直線を3つの部分に分割した中間データを作成し、分割した点をp0, p2、元の直線の端の点をp1としてベジェ曲線を計算しています。
分割の位置を調整する事で角の丸め方を変える事ができます。
極端な場合は直線の中間で分割する事で常に曲線的にキャラクターを動かす事もできます。