cocos2dでスワイプの方向、速さを検出する
前回、タップの使い方について解説しました。今回はiPhoneアプリのインタラクションの特徴でもあるスワイプを扱ってみたいと思います。
通常のCocoa Touchを用いた開発では、UIGestureRecognizer
のサブクラスUISwipeGestureRecognizer
を使って方向などを取得しますが、cocos2dにUIKit
フレームワークを入れなければならない上、SwipeGestureRecognizerはぶっちゃけ全然使いものになりません。スワイプを検出し、方向(上下左右)を決定するには特化していますが、ゲーム的な使い方には向いてないように思います。TouchBeganなどのタッチ系メソッドが反応しないので、スワイプの速さや細かな座標などを用いる処理には向いていなさそうです。
例えばオブジェクトを何処かへ飛ばすようなタッチアクションを作りたいとすると、4方向しか出せないUIGestureRecognizer
を使うよりは自分で実装した方がよさそうです。
一見難しそうですが、全然難しくありません。TouchEndedの処理だけでいけます。
スワイプ検出処理のアプローチ
スワイプというアクションを細かく考えると、「速いタッチムーブ」のあと、「タッチが離れた時」に発生するものです。ですので、ccTouchEnded
でスワイプ処理をすればよさそうです。速いタッチ移動はどのように検出すればいいのでしょうか。
ここで小学校の算数が出てきます。「速さ」=「時間」×「距離」。
スワイプの秒速を知りたければ、1秒間に移動した距離を求めればいいのです。が、別に単位が秒じゃなくてもいいですよね。コンピュータはもっともっと早く動いています。単位をフレームにして、フレーム速度を求めます。1フレームで移動した距離がわかれば、それを速さとして使えそうです。
ということで移動距離を求めないといけません。距離の算出には座標値を用います。ここからは、中学の数学です。2点の座標値がわかれば、その距離を求めることができます。でも、求め方なんて知らなくていいです。cocos2dがやってくれます。
cocos2dフレームワークのCGPointExtention
クラスにはそのようなCGPoint
型データを計算する様々な関数が用意されています。座標値の足し算や引き算だけでなく、内積や外積、様々な機能が用意されているので、今後も使う機会があると思います。ざっと見ておくと、コーディングの際に役立つと思います。
とにかく今回は2点間の距離を計算してくれる”ccpDistance
“関数を用います。この関数の引数として、2点の座標値を与えてやれば、その距離を返してくれます。
今回は、タッチが終了する直前の速さを知りたいので、ccTouchEnded
に入ってくるタッチオブジェクトの座標値を使いましょう。1フレームでの移動距離なので、終了時の位置とその一つ前の位置を取得しなければなりません。一つ前の位置は便利なことにこのタッチオブジェクトが保持してくれています。それらを用いれば最後の1フレームで移動した距離、すなわち速さが取得できます。
スワイプを検出するためには、この速さに閾値を設け、一定の速さより速いタッチムーブで終了した際はスワイプであると判断すればいいのです。
実装する
アプローチの仕方がわかった所で、実際にプログラムを書いていきましょう。ccTouchesEnded
またはccTouchEnded
に以下のように記述し、まずタッチの座標値を取得します。
-(void)ccTouchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { UITouch *touch = [[touches allObjects] objectAtIndex:0]; /* タッチが離れた時の位置 */ CGPoint currentPos = [[CCDirector sharedDirector] convertToGL:[touch locationInView:[touch view]]]; /* 1フレーム前の位置 */ CGPoint prevPos = [[CCDirector sharedDirector] convertToGL:[touch previousLocationInView:[touch view]]]; }
3行目のタッチオブジェクトをtouches変数から取り出す処理は、ccTouchEnded
メソッドの場合は必要ありません。5行目、7行目でタッチのポジションを取得し、cocos2d座標系に変換しています。
次に行うのは、取得した2点の座標の距離を求めることです。以下のように記述してください。
-(void)ccTouchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { UITouch *touch = [[touches allObjects] objectAtIndex:0]; CGPoint currentPos = [[CCDirector sharedDirector] convertToGL:[touch locationInView:[touch view]]]; CGPoint prevPos = [[CCDirector sharedDirector] convertToGL:[touch previousLocationInView:[touch view]]]; float dist = ccpDistance(currentPos, prevPos); }
ccpDistance
関数を用いて、1フレームの間で動いた距離を算出しています。
最後にこの求めた距離を閾値で判別し、閾値よりも大きければスワイプと判断するような条件式を書けばいいです。
-(void)ccTouchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { UITouch *touch = [[touches allObjects] objectAtIndex:0]; CGPoint currentPos = [[CCDirector sharedDirector] convertToGL:[touch locationInView:[touch view]]]; CGPoint prevPos = [[CCDirector sharedDirector] convertToGL:[touch previousLocationInView:[touch view]]]; float dist = ccpDistance(currentPos, prevPos); if(dist > 5.0f) { NSLog(@"swipe detected"); } }
上のif
文がスワイプを判断している部分です。今回は仮に5.0を閾値としました。これよりも大きく動いた時はスワイプと判断され、出力されます。この閾値はアプリに合わせて調整してください。
これで一応スワイプの検出はできました。
スワイプ方向や速さの算出
何かのオブジェクトをスワイプで飛ばすようなゲームを作る場合、スワイプの方向や速さを細かく知る必要があります。このような場合も先ほどの2点の座標値を用いてそれらを算出します。要はタッチエンドの際のベクトルを求めてやるわけです。
ベクトルを求めるといっても、2点の座標の差分を出すだけです。
CGPoint vec = ccpSub(currentPos, prevPos);
このベクトルを応用することで、方向や速さを求めることができます。
速さなら先程のccpDistance
関数で求めたdist
も使えます。レイヤーを2次元座標とみなした時のベクトルの角度を知りたければ以下のようにccpToAngle
関数を用います。
float angle = ccpToAngle(vec)*180/M_PI;
M_PI
はπです。3.14…が入っています。180/π
をかけているのは、ラジアンを角度で表すためです。これでベクトルの角度が−180°〜180°の範囲で表されます。
スワイプでオブジェクトを飛ばしたい時はこれらの数値を使い、スワイプを検出した時点で、ccMoveBy
などを用いて、飛ばしたいオブジェクトにアクションをさせてやればいいのです。
上下左右のスワイプを判別する
最後に一応上下左右を判別する簡単なメソッドの作り方を紹介しておきます。考え方としては、まずスワイプのベクトルから角度を算出、その角度によって上下左右の方向を割り振ります。
この上下左右はint
型の番号0,1,2,3のように割り振ればいいのですが、後でわかりやすくするために、enum
を使ってその連番も定数で管理します。
ヘッダファイルで以下のように方向の種類を示すenum
を作ります。enum
を使ったタグの管理と同様です。
typedef enum { swipeUp, swipeDown, swipeLeft, swipeRight } swipeDirection;
では次に実装部で「ベクトルを与えたら、方向を返す」メソッドを作ります。名前はswipeDirection
とでもしておきましょう。
-(unsigned) swipeDirection:(CGPoint)vec { unsigned direction; float angle = ccpToAngle(vec)*180/M_PI; if(45.0f <= angle && angle < 135.0f) //up { direction = swipeUp; NSLog(@"up"); } else if(-135.0f <= angle && angle < -45.0f) //down { direction = swipeDown; NSLog(@"down"); } else if((135.0f <= angle && angle < 180.0f) || (-180.0f <= angle && angle < -135.0f)) //left { direction = swipeLeft; NSLog(@"left"); } else if((0 <= angle && angle < 45.0f) || (-45.0f <= angle && angle < 0) ) //right { direction = swipeRight; NSLog(@"right"); } else { direction = nil; } return direction; }
返り値のunsigned
は非負のint
型です。0,1,2,3
の値しか入らないのでこのようにしておきます。4行目で取得したベクトルから角度を取得しています。あとは条件式を使い、その角度によって上下左右に割り振っています。direction
変数に入っているのはenum
で宣言している定数です。実態は上下左右の順で0,1,2,3
です。
最後はこのメソッドをタッチエンドでスワイプが検出された時に呼び出すようにすればオッケーです。
例として以下のように書いておきます。
-(void)ccTouchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { UITouch *touch = [[touches allObjects] objectAtIndex:0]; CGPoint currentPos = [[CCDirector sharedDirector] convertToGL:[touch locationInView:[touch view]]]; CGPoint prevPos = [[CCDirector sharedDirector] convertToGL:[touch previousLocationInView:[touch view]]]; float dist = ccpDistance(currentPos, prevPos); if(dist > 5.0f) { CGPoint vec = ccpSub(currentPos, prevPos); unsigned d = [self swipeDirection:vec]; NSLog(@"swipe detected - direction:%d",d); } }
スワイプを判別した時に何か処理を行う場合はswipeDirection
メソッドの中に直接書くか、swipeDirection
の返り値をswitch
か何かで分岐させて、必要な処理を呼び出すようにすれば良いです。
とにかくこのようにスワイプを検出、扱うことで、ゲームに必要な様々なアクションへ応用させることができます。