cocos2dでタップを使う
タップの検出
タッチオブジェクトの扱い方の感覚がわかってきたと思うので、今回はタップについて見ていきたいと思います。タイトルで「cocos2dで」と書いてありますが、今回はほとんどCocoa Touchの内容です。
まずはタップの検出の仕方です。と言っても、とっても簡単です。
UITouch型のタッチオブジェクトには”tapCount”というプロパティが用意されているので、そこからタップ数が取得できます。
タッチが終わった時にタップかどうかを判断するので、ccTouchesEndedまたはccTouchEndedにタップに関する処理を書きます。
具体的には以下のように用いることができます。
-(void)ccTouchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { UITouch *touch = [[touches allObjects] objectAtIndex:0]; CGPoint currentPos = [[CCDirector sharedDirector] convertToGL:[touch locationInView:[touch view]]]; if(touch.tapCount == 1) { NSLog(@"tap detected at (%f, %f)", currentPos.x, currentPos.y); } }
ifの条件式でタップカウントを調べています。このプログラムではシングルタップが起こった時のみ、タッチのポジションがログに出力されます。
ダブルタップを検出する時は、このif文に追加し、
if(touch.tapCount == 1) { /* シングルタップの処理 */ } else if(touch.tapCount == 2) { /* ダブルタップの処理 */ }
のように扱うか、switchを使って、
switch(touch.tapCount) { case 1: /* シングルタップの処理 */ break; case 2: /* ダブルタップの処理 */ break; default: /* タップ以外の処理 */ break; }
のように書くことができます。
シングルタップとダブルタップを混在させる方法
このようにタップの数を取得することは非常に容易にできます。しかし、このままではシングルタップとダブルタップを混在させた時にある問題が発生します。
それはダブルタップの時でも、シングルタップの処理をまず実行してしまうということです。
ダブルタップが検出されるまでには、
タッチ開始→タッチ終了→タッチ開始→タッチ終了
というプロセスが実際には発生し、2回目のタッチ終了で初めてtapCountが2に設定されるのです。したがって1度目のタッチ終了でccTouchesEndedメソッドはシングルタップの処理を実行し、2回目でダブルタップの処理を実行します。
これを回避するためには、シングルタップの処理の実行を少し遅らせ、ダブルタップが発生しなかった場合に実行させるようにします。
ダブルタップが発生した場合は、遅らせている処理をキャンセルし、ダブルタップの処理を実行します。
実際にプログラムを書きながら、どのように実装するのか見て行きましょう。
まず先ほどのccTouchesEndedメソッドに加え、シングルタップの時に実行する処理と、ダブルタップの時に実行する処理を一つのメソッドとして分けておきましょう。
-(void)ccTouchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { UITouch *touch = [[touches allObjects] objectAtIndex:0]; CGPoint currentPos = [[CCDirector sharedDirector] convertToGL:[touch locationInView:[touch view]]]; switch (touch.tapCount) { case 1: [self singleTapMethod:currentPos]; break; case 2: [self doubleTapMethod:currentPos]; break; default: break; } } -(void)singleTapMethod:(CGPoint)touchPos { NSLog(@"single tap detected at (%f, %f)", touchPos.x, touchPos.y); } -(void)doubleTapMethod:(CGPoint)touchPos { NSLog(@"double tap at (%f, %f)", touchPos.x, touchPos.y); }
ccTouchesEndedメソッド内ではタップカウントに応じてそれぞれのメソッドへメッセージを送っています。
このままこれを実行すると、先ほど述べたように、ダブルタップの時には”singleTapMethod”と”doubleTapMethod”が呼ばれます。
そこで遅延の処理をswitchのcase 1内に記述します。
-(void)ccTouchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { UITouch *touch = [[touches allObjects] objectAtIndex:0]; CGPoint currentPos = [[CCDirector sharedDirector] convertToGL:[touch locationInView:[touch view]]]; switch (touch.tapCount) { case 1: [self performSelector:@selector(singleTapMethod:) withObject:[NSValue valueWithCGPoint:currentPos] afterDelay:0.3]; break; case 2: [self doubleTapMethod:currentPos]; break; default: break; } }
初めて見るとちょっと一歩下がってしまう文ですよね。簡単に解説します。
セレクタとは
まずここではセレクタという概念を知っておかなければなりません。
ま、言うならばメソッド名のことなんですが、メソッド名とキーワードをまとめたものをセレクタと言います。「ccTouchesEnded:withEvent」も”ccTouchesEnded”がメソッド名、”withEvent”の部分がキーワードとなり、このひと固まりでセレクタになります。
Objective-Cではこのセレクタをデータ型として扱うことができます。要はそのメソッドが他のメソッドの引数になり得るということです。
難しく書いていても仕方ないので、実際のコードを見てみましょう。
先ほどのコードの「performSelector」というのはNSObjectクラスに定義されているメソッドですが、そのメソッドの引数を見てみましょう。singleTapMethodと書かれています。
つまり、メソッドが引数として他のメソッドに渡されているのです。
「singleTapMethod」の前に書かれている「@selector」というのはコンパイラ指示子で、ここではsingleTapMethodを一つのデータとして扱うよということをコンパイラに明示しています。
ここまでの説明でなんとなくわかったかと思いますが、performSelectorメソッドはその名のとおり「引数で取得するセレクタをパフォーム(実行)する」メソッドなのです。
withObjectとは
その後に続く「withObject」というのは、performSelectorのキーワードの一つで、引数で取得しているセレクタへ渡すオブジェクトを指定します。
ここではすなわち、singleTapMethodに渡す引数です。singleTapMethodの定義を見てみるとCGPoint型のデータを引数に必要としています。
しかし、withObjectはそのなのとおりオブジェクト型しか取り扱ってくれません。
そこで解決策として、「CGPoint型をオブジェクト型に一旦変換して渡してやり、目的のセレクタに渡ってからもう一度CGPoint型に変換し直す」という方法を取ります。
そのオブジェクト型に変換しているのが
[NSValue valueWithCGPoint:currentPos]
の部分です。NSValueというintやfloatなどの様々なデータをオブジェクトとして保持してくれるデータに変換して、セレクタに渡してやります。
このオブジェクトを引数としてとるためにsingleTapMethodも少し変更してやらないといけません。
-(void)singleTapMethod:(NSValue*)touchPosObj { CGPoint touchPos = [touchPosObj CGPointValue]; NSLog(@"single tap detected at (%f, %f)", touchPos.x, touchPos.y); }
まず引数をCGPoint型からNSValue*型に変更してやります。
そして今度はメソッド内で、取得したNSValueをCGPointに変換してやります。このようにすることでCGPointでもセレクタに渡すことができます。
afterDelayで遅延の長さを指定
もう一度performSelectorの文に戻りましょう。最後に書かれているキーワード”afterDelay”が一番の重要ポイントで、遅延時間を指定している部分です。
単位は秒です。ここでは0.3秒を指定しました。
こう書くことで、タップが発生してから、0.3秒遅らせてsingleTapMethodを実行することができます。
指定する秒数は自分の感覚でアプリを実行しながら決めてください。アプリによっては遅いと感じるかもしれません。その時は0.2秒などに設定しておけばいいでしょう。
ダブルタップ時の処理
かなり長くなって来ましたが、もうひと踏ん張りです。
ダブルタップが発生した時の処理も少し変更しなければなりません。
まず行うことは、待機状態になっているsingleTapMethodの実行をキャンセルすることです。これを行わなければ、結局ダブルタップした時でも、0.3秒後singleTapMethodは実行されます。
キャンセルは以下のように行うことができます。
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(singleTapMethod:) object:[NSValue valueWithCGPoint:currentPos]];
また長い文が出て来ましたが、先ほどのperformSelectorと構造は基本的に同じです。
初めの引数selfはキャンセルしたいセレクタの書いてある場所です。そして次の引数でキャンセルしたいセレクタ、次にそのセレクタが引数を持っているなら、その引数を指定します。
このようにシングルタップのメソッドをキャンセルした後、ダブルタップの処理を実行してやります。
全体のプログラムを最後に載せておきます。
-(void)ccTouchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { UITouch *touch = [[touches allObjects] objectAtIndex:0]; CGPoint currentPos = [[CCDirector sharedDirector] convertToGL:[touch locationInView:[touch view]]]; switch (touch.tapCount) { case 1: [self performSelector:@selector(singleTapMethod:) withObject:[NSValue valueWithCGPoint:currentPos] afterDelay:0.3]; break; case 2: [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(singleTapMethod:) object:[NSValue valueWithCGPoint:currentPos]]; [self doubleTapMethod:currentPos]; break; default: break; } } -(void)singleTapMethod:(NSValue*)touchPosObj { CGPoint touchPos = [touchPosObj CGPointValue]; NSLog(@"single tap detected at (%f, %f)", touchPos.x, touchPos.y); } -(void)doubleTapMethod:(CGPoint)touchPos { NSLog(@"double tap detected at (%f, %f)", touchPos.x, touchPos.y); }
13行目がキャンセルしている部分で、14行目でdoubleTapMethodを呼び出し、実行しています。
一見ややこしい処理をしているようですが、セレクタの概念さえわかればなんてことないです。
タップの処理はこのように混在させることができます。
次回はiPhoneのタッチの中でも重要なスワイプを扱ってみたいと思います。
追加
実機では1回目のタップで発生したsingleTapMethodがキャンセルされないようです。
これはsingleTapMethodをperformSelectorで呼び出す際にセレクタに渡したcurrentPosと、ダブルタップ時にキャンセルする際に渡すcurrentPosが異なってしまうために発生しているようです。
シミュレータではカーソルを動かさない限り、同じ位置をタップできますが、実機では不可能に近いですからね。
解決策として一番簡単なのは(パッと思いつくのは)、シングルタップ時の座標値を変数に入れて保持しておき、キャンセルの際はその座標値を用いてキャンセルするという方法です。
(previousLocationInViewで前回のポジションを取得し、キャンセル時にそれを渡したのですが、うまくいきませんでした。)
保持する変数はインスタンス変数として作ればいいと思います。
/* HelloWorldLayer.m */ @implementation HelloWorldLayer { CGPoint firstTapPos; }
作る変数は隠蔽でいいと思うので、実装部の方で上記のように宣言しておきます。で、ccTouchEndedの方で以下のように記述しておきます。
switch (touch.tapCount) { case 1: [self performSelector:@selector(singleTapMethod:) withObject:[NSValue valueWithCGPoint:currentPos] afterDelay:0.3]; firstTapPos = currentPos; break; case 2: [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(singleTapMethod:) object:[NSValue valueWithCGPoint:firstTapPos]]; [self doubleTapMethod:currentPos]; break; default: break; }
シングルタップ時に、先ほど生成したfirstTapPos変数にcurrentPosを入れておき、キャンセルする時はそのfirstTapPosを渡してやります。
これで実機でもちゃんとキャンセルしてくれるようになりました。
はじめまして。
大変参考になります!
上記ダブルタップのコードを実行したところ、シミュレータではsingleTapMethod:はキャンセルされるのですが、実機ですとsingleTapMethod:が呼ばれてしまいます。
何か問題があるのでしょうか?
@たっつんさん
ご指摘ありがとうございます。確認したところ、実機では確かに初めのタップがキャンセルされていませんでした。
これは実機でのタップだと2回目のタップの座標値が1回目と異なるため、違うセレクタとみなされキャンセルする対象になっていないという事がわかりました。
解決方法としては、1回目のタップの座標値を変数か何かで保持しておき、それをキャンセル時に渡してやるのが一番簡単なやり方かと思います。
詳しくは本文を修正しておきますので、ご参考ください。
修正ソースにて確認を行い、実機でキャンセルされることを確認しました。
ありがとうございました!!