12月 8, 2011   51 リアクション

CCMotionStreakを使えばライントレースアプリも簡単

cocos2d Advent Calendar 2011の、第8日目を担当するhkato193です。

ATND:cocos2d Advent Calendar 2011
前日の記事:FacebookのOAuth認証のライブラリ「Facebook Connect」をcocos2dで使用する方法(@hmbrwさん)

0.はじめに

iOSアプリでflightControlという有名なゲームがあります。このゲーム、Webに転がっている動画を見てもらうと分かるように、飛行機のルートを指でなぞることで作り、画面内に出てくるたくさんの飛行機を上手に滑走路に誘導してあげるゲームです。

このゲームのアルゴリズムと面白さにおける肝は「指でなぞるとルートが描かれる」と「描いたルートのとおりにオブジェクトが動く」にあるわけですが、今回はこれをcocos2dでも実現してみましょう。

使用するcocos2dのクラスは、主に

の3つです。

なお、本記事のアルゴリズムは、下記サイトのサンプルプログラムを参考にしています。この方のブログにはcocos2dの面白い記事が色々とありますので、一度ご覧になるのをおすすめします。

Simple line follow demo (CCMotionStreak) | SuperSuRaccoon’s World

本記事の説明に使用したコードはgithubからダウンロードできます。

katokichisoft/mimicontrol - GitHub

1. タッチした軌跡を画面に描画する

タッチの跡が画面上に残るようにするには、CCRibbonのサブクラスであるCCMotionStreakを使うのが簡単です。このマイナーなクラスは、内部で保持しているCCRibbonオブジェクトの以下の機能:

という特徴を活用して、

という機能を提供しています。つまり、CCMotionStreakオブジェクトの現在位置をアプリ動作中に随時更新していけば、それだけで線になる便利なクラスです。フェードアウトの時間も設定出来ますのでモーションブラーのような効果を実現するときにも役立ちますね。今回の例のように線を描きたい場合、フェードアウトまでの時間を長く取った消えないCCMotionStreakオブジェクトを、タッチイベントと共に場所移動させていけばOKです。

ソースコードだとこんな感じになります。

- (BOOL)ccTouchBegan:(UITouch *)touch withEvent:(UIEvent *)event
{
    CGPoint touchLocation = [touch locationInView: [touch view]];
    CGPoint curPosition = [[CCDirector sharedDirector] convertToGL:touchLocation];   

    CCMotionStreak* line = [CCMotionStreak
                                streakWithFade:99999
                                         minSeg:1
                                          image:@"line.png"
                                          width:6
                                         length:10
                                          color:ccc4(255,255,255,255)];
    line.position = curPosition;
    [self addChild:line z:5 tag:193];

    return YES;
}

- (void)ccTouchMoved:(UITouch *)touch withEvent:(UIEvent *)event
{
    CGPoint touchLocation = [touch locationInView: [touch view]];
    CGPoint curPosition = [[CCDirector sharedDirector] convertToGL:touchLocation];

    CCMotionStreak* streak = (CCMotionStreak *)[self getChildByTag:193];
    streak.position = curPosition;
}

- (void)ccTouchEnded:(UITouch *)touch withEvent:(UIEvent *)event  
{
    // 今は何もしない
}

ccTouchBegan:withEvent:でフェードまでの時間が非常に長いCCMotionStreakオブジェクトを作成してタッチ位置に配置、以降のccTouchMoved:withEvent:でオブジェクトの場所を更新しています。lineはその都度getChildByTag:で取ってきています。

cocos2dでは頻繁に増減するオブジェクトはタグで管理するのが一般的ですので、ここではそれにならっています。もちろん検索のコストが若干でも気になる方はメンバ変数にして管理しても構いません(このサンプルでも、次に示すPlaneオブジェクトはメンバ変数で管理して、アクセスの手間を減らしています)。

ラインには縦方向で白色の画像を用意しておきます(そのままだと白くて見えないので、背景を黒くしています)。この画像をCCMotionStreakクラスに渡すことで、あんばいよく画像を配置してくれるわけです。

20111208182753

なお、ccTouchBegan:withEvent:で、CCMotionStreakオブジェクト生成前にremoveChildByTag:cleanup:を呼んでいるのは、画面上にラインが1本だけ描画されるようにするためです。removeChildByTag:cleanup:は、削除するオブジェクトが無い場合には何もしないので、条件分岐によるヌル判定は必要ありません(厳密にはCCNode.mの中で警告を出していますが、今回のようなケースでは無視しても構いません)。

ここまでを実行したときのスクリーンショットはこんな感じになります。

ついでですので、オブジェクトの生成時に渡すパラメータを色々と変更して、CCMotionStreakオブジェクトがどのように振る舞うかを試してみると良いでしょう。

2. 軌跡に沿ってノードを移動する

軌跡の位置情報や、各ポイントの回転角度は前出のCCMotionStreakクラスが知っているのですが、中に隠蔽されてしまっているため取り出すのは面倒です。cocos2dのソースに手を入れても良いのですが、バージョンアップの度に移植作業が発生するのは何かと面倒です。

ですので、今回は移動するノードに配列を持たせ、CCRibbonと同様に移動先の座標群を知っておくようにします。

では移動するクラスを作成しましょう。Plane.h, Plane.mを作成し、インタフェースを次のように定めます。

#import 
#import "cocos2d.h"

@interface Plane : CCSprite {

    CCArray *linePathPosition_; // 移動先の座標群
    BOOL isSelected_;           // 現在選択されているか
    BOOL isMoving_;             // 現在移動中かどうか
    
    float velocity_;  // 移動速度
}
-(void) updatePosition:(CGPoint)position;
-(void) start;
-(void) stop;

@property (nonatomic, assign) BOOL isSelected;
@end

動作開始/停止の合図によって、外から与えられた移動先の座標に沿って自立動作することを意図したインタフェースにしています。

実装部Plane.mの要点を以下に記します。詳細はGithubのサンプルコードを参照してください。

-(void) addPosition:(CGPoint) position
{
    [paths_ addObject:[NSValue valueWithCGPoint:position]];
}

- (void) start
{
    if (!isMoving_) {
        if ([paths_ count] > 0) {
            
            isMoving_ = YES;
            CGPoint target = [[paths_ objectAtIndex:0] CGPointValue];
            
            float angle = CC_RADIANS_TO_DEGREES(atan2(self.position.y - target.y, self.position.x - target.x));
            angle += 90;
            angle *= -1;
            self.rotation = angle;
            
            [self runAction:[CCSequence actions:
                             [CCMoveTo actionWithDuration:ccpDistance(self.position,target)/velocity_
                                                 position:target],
                             [CCCallFunc actionWithTarget:self 
                                                 selector:@selector(getNext)],
                             nil]];
        }
        else
        {
            isMoving_ = NO;
        }
    }
}

- (void) stop
{
    [paths_ removeAllObjects];
}

- (void) getNext
{
    if ([paths_ count] > 0) {
        isMoving_ = NO;
        [paths_ removeObjectAtIndex:0];
    }
    [self start];
}

配列positionsに対して追加されたCGPoint型の座標群を順に辿るために、runメソッドとremoveTargetとを繰り返し呼びます。CCSequenceクラスはアクションを順番に行わせられるクラスなので、「次の座標へ移動」してから「移動先で次アクション(=その次の座標への移動)をスケジュール」というように呼ばれます。

2点の角度を計算する部分は次のようにして求めています。計算結果はrotationプロパティにセットしておくだけで、描画の時に回転情報として自動的に使われるので、ラクチンです。

float angle = CC_RADIANS_TO_DEGREES(atan2(current.y - target.y, current.x - target.x));
angle += 90;
angle *= -1;
self.rotation = angle;

このロジックによって、機首が次に移動する座標を向くようになっているのが分かりますでしょうか?

また、flightControlと同様に飛行機をタップしたときだけ線を描画させるために、タップでisSelectedの値を更新させる処理も記述します。

- (CGRect)myRect
{
    CGSize s = [self.texture contentSize];
    return CGRectMake(-s.width / 2, -s.height / 2, s.width, s.height);
}

- (BOOL)ccTouchBegan:(UITouch *)touch withEvent:(UIEvent *)event
{
    if ( !CGRectContainsPoint([self myRect], [self convertTouchToNodeSpaceAR:touch]) )   
    {
        isSelected_ = NO;
        return NO;
    }

    isSelected_ = YES;
    return YES;
}

- (void) onEnter {
    // シーン上に追加されたタイミングで、タッチ操作を有効にする
    [[CCTouchDispatcher sharedDispatcher] addTargetedDelegate:self
                                                     priority:0
                                              swallowsTouches:NO];
    [super onEnter]; // 呼び忘れないように!
}
- (void) onExit {
    // タッチ操作を無効化
    [[CCTouchDispatcher sharedDispatcher] addTargetedDelegate:self
                                                     priority:0
                                              swallowsTouches:NO];
    [super onExit]; // 呼び忘れないように!
}

PlaneクラスはCCSpriteのサブクラスですが、上のようにCCTouchDispatcherに自分を登録すれば、何の問題もなくタッチ処理をハンドリングできるようになります。そして、先ほどのタッチイベント処理の部分に、ノードに対して座標を追加する処理を記述します。

まず、タッチ開始部分。

- (BOOL)ccTouchBegan:(UITouch *)touch withEvent:(UIEvent *)event
{
    Ladybug *plane = (Ladybug *)[self getChildByTag:100];
    if (plane.isSelected) {      // 追加
        [plane stop];            // 追加
        [self removeChildByTag:193 cleanup:YES];
        CCMotionStreak* streak = [CCMotionStreak
                                     streakWithFade:99999
                                             minSeg:1
                                              image:@"line.png"
                                              width:6
                                             length:10
                                              color:ccc4(255,255,255,255)];
        streak.position = plane.position;  // 変更
        [self addChild:streak z:99 tag:193];
    }
    
    return YES;
}

先ほどのソースでは画面のどこからでもラインを描けましたが、flightControlと同じにするためにPlaneが選択状態にあるかどうかを確認した上でラインを描き始めています。これによりPlaneオブジェクトを移動させることが分かりやすいUIになります。

次いでタッチの継続と終了の部分。

- (void)ccTouchMoved:(UITouch *)touch withEvent:(UIEvent *)event {
    CCMotionStreak* streak = (CCMotionStreak *)[self getChildByTag:193];
    CGPoint touchLocation = [touch locationInView: [touch view]];
    CGPoint curPosition = [[CCDirector sharedDirector] convertToGL:touchLocation];

    Plane *plane = (Plane *)[self getChildByTag:100]; // 追加
    if (plane.isSelected) {                           // 追加
        streak.position = curPosition;                // 追加
        [plane updatePosition:curPosition];           // 追加
        [plane run];                                  // 追加
    }
}

- (void)ccTouchEnded:(UITouch *)touch withEvent:(UIEvent *)event {
    Plane *plane = (Plane *)[self getChildByTag:100]; // 追加
    plane.isSelected = NO;                            // 追加
}

方針はccTouchBegan:withEvent:メソッドと同じです。

たったこれだけで完成です。実行してみると、指でなぞって描かれた線の上を上手に飛ぶ飛行機が見られるはずです。;-)

20111208182755

ちょっとしたテクニックになりますが、上図の「cocos2d Advent…」がイタリックになっているのは、CCLabelのskewXで角度(degree)を設定しているだけです。CCLabelはインスタンス化時、あるいは新しい文字列をしていしたときに、その都度CoreGraphicsを使って画像化しているので、このような変形も非破壊的に行えます(もちろん、アクションも)。

3. 応用

4. まとめ

5. 参考情報

- 「AWMotionStreak」

CCMotionStreakと同様の機能を、より高いパフォーマンスで実現出来るクラスです。フォーラム(http://www.cocos2d-iphone.org/forum/topic/14854)によるとgles20ブランチにマージされたようなので、将来のcocos2dではCCMotionStreakがもっと使いやすくなるでしょう。

- 【この機に乗じて】「cocos2dで作るiPhone&iPadゲームプログラミング」【宣伝】

CCRibbon, CCMotionStreakなどのクラスは、本書で少しだけ紹介されています。この本、cocos2dを知っている人が中〜上級のcocos2d使いになるのに最適な本です。監修として、ほんの少しですがお手伝いさせてもらいました。

- 【調子に乗って】「cocos2d for iPhoneレッスンノート」【宣伝2】

@akio0911さんとの共著です。最初に紹介した本よりも入門者向けの内容に振ってあり、cocos2dって何という方から、何か動くものを作ってみたいという方まで、cocos2dに慣れるまでのお供をしてくれます(多分)。

6. 次の発表者へのリンク

@myb さんによる、「CCMenuでラベル付きボタン、長押しボタン」という記事です。cocos2dの機能をちょっと拡張したいようなときに、何を・どこを変えたらよいのかが分かる良い記事です。

Tweet
  1. alanah-robinsonkatokichisoftからリブログしました
  2. karyn-richardsonkatokichisoftからリブログしました
  3. katokichisoftの投稿です