CCMenuでラベル付きボタン、長押しボタン #cocos2d_2011_adcal

@yoichinejiさん主催のcocos2d Advent Calendar 2011に乗っかってみたので、久しぶりにブログを書きます。
クリスマスまでの毎日、cocos2dに関するTipsを1日1つブログにするというイベントです。
前日の記事: CCMotionStreakを使えばライントレースアプリも簡単 (@hkato193さん)

cocos2dを使っていてよくやることの、本当にちょっとしたTipsなので、すでにご存知の方はご容赦を...。
サンプルコードはこちらからダウンロードできます。


ラベル付きボタン

cocos2dでボタンを実装するときにはCCMenuを使います。
押されていない状態(normal)、押された状態(selected)、押せない状態(disabled)のそれぞれの画像(スプライト)を設定するだけで簡単にボタンを実装できます。

CCSprite *normalSprite = [CCSprite spriteWithFile:@"button_normal.png"];
CCSprite *selectedSprite = [CCSprite spriteWithFile:@"button_selected.png"];
CCSprite *disabledSprite = [CCSprite spriteWithFile:@"button_disabled.png"];

CCMenuItemSprite *menuItem = [CCMenuItemSprite itemFromNormalSprite:normalSprite 
                                                     selectedSprite:selectedSprite 
                                                     disabledSprite:disabledSprite 
                                                             target:self 
                                                           selector:@selector(didPressButton:)];
CCMenu *menu = [CCMenu menuWithItems:menuItem, nil];

selected、disabledの画像を別に作らなくても、スプライトを半透明にすることで見た目を変えることはよくやります。

CCSprite *normalSprite = [CCSprite spriteWithFile:@"button.png"];
CCSprite *selectedSprite = [CCSprite spriteWithFile:@"button.png"];
selectedSprite.opacity = 0x7f; // 半透明にする
CCSprite *disabledSprite = [CCSprite spriteWithFile:@"button.png"];
disabledSprite.opacity = 0x7f; // 半透明にする

CCMenuItemSprite *menuItem = [CCMenuItemSprite itemFromNormalSprite:normalSprite 
                                                     selectedSprite:selectedSprite 
                                                     disabledSprite:disabledSprite 
                                                             target:self 
                                                           selector:@selector(didPressButton:)];
CCMenu *menu = [CCMenu menuWithItems:menuItem, nil];

また、メニューなど形は同じで書かれている文字が違うだけのボタンをいくつも用意する、ということもよくあります。
そんなときはCCMenuItemの子にCCLabelを追加します。

CCLabelTTF *label = [CCLabelTTF labelWithString:@"PUSH!" 
                                       fontName:@"Arial" fontSize:20.0f];
[menuItem addChild:label];

さてここで問題なのですが、半透明にしたスプライトをselected、disabledの画像に設定しても文字の方は半透明にならないので、ちょっと浮いてしまいます。

ccadcal01.png (左:通常の状態、右:ボタンが押された状態)

そこで、CCMenuItemSpriteを拡張して、ボタンのスプライトが切り替わるタイミングで子要素にもスプライトの透明度が反映されるようにしてやります。

@implementation ExMenuItemSprite

- (void)adaptChildrenAppearanceTo:(CCNode  *)target
{
    for(CCNode *node in children_)
    {
        if([node isEqual:normalImage_] || [node isEqual:selectedImage_] || [node isEqual:disabledImage_])
            continue;
        
        if(![node conformsToProtocol:@protocol(CCRGBAProtocol)])
            continue;
        
        [(CCNode  *)node setOpacity:[target opacity]];
    }
}

- (void)selected
{
    [super selected];
    [self adaptChildrenAppearanceTo:selectedImage_];
}

- (void)unselected
{
    [super unselected];
    [self adaptChildrenAppearanceTo:normalImage_];
}

- (void)setIsEnabled:(BOOL)enabled
{
    [super setIsEnabled:enabled];
    
    if(enabled)
    {
        if(self.isSelected)
            [self adaptChildrenAppearanceTo:selectedImage_];
        else
            [self adaptChildrenAppearanceTo:normalImage_];
    }
    else
    {
        if(self.disabledImage)
            [self adaptChildrenAppearanceTo:disabledImage_];
        else
            [self adaptChildrenAppearanceTo:normalImage_];
    }
}

@end

adaptChildrenAppearanceTo:というメソッドの引数に画像を渡すと、その画像と同じ透明度を子要素に設定していきます。
あとはボタンが押されたとき(selected)、離されたとき(unselected)、使用可・不可状態が変わったとき(setIsEnabled:)に、状況に合った画像を引数にしてadaptChildrenAppearanceTo:を呼べばいい、という寸法です。

ccadcal02.png これでバッチリ!


長押しボタン

指をなるべく動かさず素早くボタンの機能を切り替える、というようなことが必要なシチュエーションがあったので、長押しボタンを実装してみました。
CCMenuではタッチイベントは個々のボタンであるCCMenuItemではなく、CCMenuで扱っています。
そこでCCMenuを拡張して、タッチの開始時にタイマーを仕込み、タッチが終了する前にタイマーが作動したら長押しされたことにするようにしました。

@implementation ExMenu

- (id)init
{
    if((self = [super init]))
    {
        longPressTimer = nil;
        longPressState = kExMenuLongPressStateNone;
    }
    return self;
}

- (void)dealloc
{
    if(longPressTimer != nil)
        [longPressTimer invalidate];
    
    [super dealloc];
}

- (BOOL)ccTouchBegan:(UITouch *)touch withEvent:(UIEvent *)event
{
    BOOL result = [super ccTouchBegan:touch withEvent:event];
    if(result)
    {
        if(longPressTimer != nil)
            [longPressTimer invalidate];
        
        longPressTimer = [NSTimer scheduledTimerWithTimeInterval:kExMenuLongPressInterval 
                                                          target:self selector:@selector(didFireLongPressTimer:) 
                                                        userInfo:nil repeats:NO];
        longPressState = kExMenuLongPressStateBegan;
    }
    else
    {
        longPressState = kExMenuLongPressStateNone;
    }
    
    return result;
}

-(void) ccTouchMoved:(UITouch *)touch withEvent:(UIEvent *)event
{
    if(longPressState == kExMenuLongPressStateFired)
    {
        // didFireLongPressTimerでccTouchEndedを呼ぶのでここでは呼ばない
    }
    else
    {
        [super ccTouchMoved:touch withEvent:event];
    }
}

- (void)ccTouchEnded:(UITouch *)touch withEvent:(UIEvent *)event
{
    if(longPressTimer != nil)
    {
        [longPressTimer invalidate];
        longPressTimer = nil;
    }
    
    if(longPressState == kExMenuLongPressStateFired)
    {
        // didFireLongPressTimerでccTouchEndedを呼ぶのでここでは呼ばない
    }
    else
    {
        longPressState = kExMenuLongPressStateNone;
        [super ccTouchEnded:touch withEvent:event];
    }
}

- (void)ccTouchCancelled:(UITouch *)touch withEvent:(UIEvent *)event
{
    if(longPressTimer != nil)
    {
        [longPressTimer invalidate];
        longPressTimer = nil;
    }
    
    longPressState = kExMenuLongPressStateNone;
    [super ccTouchCancelled:touch withEvent:event];
}

- (void)didFireLongPressTimer:(NSTimer *)timer
{
    longPressTimer = nil;
    
    if(longPressState == kExMenuLongPressStateBegan)
    {
        longPressState = kExMenuLongPressStateFired;
        [super ccTouchEnded:nil withEvent:nil];
    }
    else
    {
        longPressState = kExMenuLongPressStateNone;
    }
}

- (BOOL)isLongPress
{
    return (longPressState == kExMenuLongPressStateFired);
}

@end

ccTouchBegan:withEvent:でNSTimerのインスタンスを作って、一定時間後にdidFireLongPressTimer:が呼ばれるようにタイマーを始動します。
didFireLongPressTimer:ではlongPressStateに長押しが成立したという状態を記憶していて、superのccTouchEnded:withEvent:を呼びます。
すると、押されたボタンに設定されているセレクタが通常通り呼ばれるので、ボタンの親であるExMenuのisLongPressメソッドを参照して処理を分岐させることができる、という仕組みです。

- (void)didPressButton:(CCMenuItem *)sender
{
     ExMenu *menu = (ExMenu *)sender.parent;
     if([menu isLongPress])
     {
          // 長押しされた!
     }
}

サンプルでは、CCMenuItemToggleを使ってボタンを長押しするともうひとつのボタンとトグルで切り替わるようにしてみました。

ccadcal03.png


さて、cocos2d Advent Calendar 2011もここから2周目に入りそうです。
まだまだ参加枠はあるようなので小粋なTipsをお持ちの方、ぜひ参加してみませんか?

次の記事: cocos2dキャラクタークラス設計の考察 (@ajinotatakiさん)