cocos3d+Bulletで物理シミュレーション (導入編) #cocos2d_2011_adcal

@yoichinejiさん主催のcocos2d Advent Calendar 2011、16日目の記事です。
前日の記事: CCSendMessagesでもっと簡潔に直感的なコードを書こう (@Seasonsさん)

今回はcocos2d上で3Dオブジェクトを扱えるフレームワーク「cocos3d」とオープンソースの物理演算エンジン「Bullet」を使って物理シミュレーションをしてみます。
サンプルコードはこちらからダウンロードできます。


cocos3dの導入

cocos3dはhttp://brenwill.com/cocos3d/の右カラムの「DOWNLOAD COCOS3D」からダウンロードできます。
執筆時点での最新バージョンは0.6.4です。

Terminalで解凍したフォルダへ移動し、cocos2dのパスを指定してインストールスクリプトを実行すると、Xcodeのテンプレートがインストールされます。
この操作は管理者権限で実行する必要があります。

$ cd cocos3d\ 0.6.4
$ ./install-cocos3d.sh -u -f -2 "../cocos2d-iphone-1.0.1"


cocos3dのプロジェクトを作成する

Xcodeのメニュー File → New → New Project... を選択し、iOS / cocos3dから「cocos3d Application」を選択します。
プロジェクト名を付けて適当な場所に保存します。

ccadcal16_01.png

これをそのままビルド・実行してみるとHello worldの文字が回転します。

ccadcal16_02.png


Bulletの導入

Bulletはhttp://bulletphysics.org/wordpress/の左上の「Bullet Download」からダウンロードできます。
執筆時点での最新バージョンは2.79です。

今回は、Bulletを静的ライブラリとして導入します。
解凍したフォルダ内にあるsrcフォルダをプロジェクトフォルダにコピーして、名前をBulletに変更しておきます。
Project Navigatorでプロジェクトを選択して下のAdd Targetをクリック、Mac OS X / Framework & Libraryから「C/C++ Library」を選択します。

ccadcal16_03.png

Product Nameは「Bullet」、Typeは「Static」にして、Use Automatic Reference Countingはオフにしておきます。

ccadcal16_04.png

Xcodeのメニュー File → Add Files to "<プロジェクト名>"... を選択して、Bulletのソースファイルをプロジェクトに追加します。
このときAdd to targetsで「Bullet」にチェックして、先ほどのBulletフォルダを指定します。

ccadcal16_05.png

Project Navigatorでビルドしないファイルの参照をごそっと削除します。

ccadcal16_06.png

プロジェクトのBuild Settingsを変更します。

  • Header Search PathsにBulletフォルダへのパス($SRCROOT/$PROJECT_NAME/Bullet)をRecursiveで追加。

BulletのターゲットのBuild Settingsを変更します。

  • Base SDKを「Latest iOS」に変更。
  • Architecturesを「Standard (armv7)」に変更。
  • Other Linker Flagsから「-lz」を外す。

アプリケーションターゲットのBuild Settingsを変更します。

  • Build Phases → Target Dependencies の+ボタンを押してBulletを追加。
  • Build Phases → Link Binary With Libraries の+ボタンを押してlibBullet.aを追加。

さて、ようやくこれでコードを書く準備ができました。
思いのほか長くなってしまったのでここまでを導入編として、実際の物理シミュレーションの実装は次の機会にしたいと思います。

ccadcal16_07.png
こんな感じになります!

次の記事: よく分かってない人がgoogle先生と一緒につくるはじめてのBox2D (@marchEnterpriseさん)

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さん)

Visual Studio 2010 ProfessionalでWindows Phone 7の開発。

もろもろ事情があってWindows Phone 7の開発環境を整えていますが、その際にちょっとはまったことをメモ。

元々MacParallelsで入れたWindows 7上にVisual Studio 2010 Professionalを入れていたので、これで開発すればいいかー、と思っていたら、WP7のSDKはVS 2010 Express、Expression Blendなどがオール・イン・ワン・パッケージになっていて、普通にインストールしただけではテンプレートがProfessionalの方には適用されませんでした。

メニューは英語だけどまあいいかと、HelloWorldを作るところまではExpressを使っていましたが、本格的に開発を始めようとしたところでExpressではインデントにタブコードを挿入できないらしいことに気がつき、以下の記事を参考にProfessionalの方にWP7のテンプレートを入れました。

Phone 7 に届くまで #52:VS2010 日本語環境にWP7のテンプレートを入れる - 高橋 忍のブログ - Site Home - MSDN Blogs

2.と3.の間にシステムを再起動しないと4.がうまく実行されませんでしたが、これでタブでインデントもできて、メニューも日本語に。

ちなみに、デスクトップのSilverlightと同じように開発できるらしいので、こちらの記事が参考になるかもしれませんよ!

有料iPhone/iPadアプリの3月分売り上げを義援金として寄付しました。

Appleからの有料iPhone/iPadアプリの3月分売り上げ入金額の明細が届いたので、振り込みに行ってきました。

IMG_1609.JPG

売り上げは31,330円、手数料等が4,000円で差し引き27,330円の入金だったので、27,330円を日本赤十字社に寄付しました。微々たる金額ですが、復興の一助になればと思います。

今後もアプリを通じて復興の支援をしていきたいと思います。

banner.jpg

株式会社フォーユーさんの制作されたバナーを使わせていただいています。ありがとうございます!

東北地方太平洋沖地震に際して、有料iPhone/iPadアプリの3月分売り上げ全額を寄付することにしました。

東北地方太平洋沖地震で被災された方々に、心からお見舞いを申し上げます。

微力ながら復興の一助になればと思い、現在リリースしている有料のiPhone/iPadアプリの3月分売り上げを、手数料を引いてAppleから振り込まれた金額全額を義援金として寄付することにしました。(寄付先は今のところ、日本赤十字社を予定しています)

3月分の売り上げが確定して振り込まれるのは、4月末ごろの見込みです。そのころにまた結果を報告したいと思います。

対象のアプリはこちらです:

se_icon_75.jpg
Spam Eater

メールサーバー上でスパムメールを除去するツール。(*POP3のみ対応)

tt_icon_75.jpg
The Twins

iPad専用!上下のバーで打ち返すブロック崩し

こちらの記事で触れられていますが、寄付"だけ"を目的にアプリを購入することは効率がよろしくないです。アプリが気になった方はぜひ購入してください。

その他にも、現在位置をメールなどで知らせることができるアプリ、"Signal Fire"も無料でリリースしています。こちらもお役に立てれば。

sf_icon_75.jpg

Signal Fire

banner.jpg

株式会社フォーユーさんの制作されたバナーを使わせていただいています。ありがとうございます!

2011-03-16 バナーのリンク先を変更。バナーの配布元を追加。

6th gen iPod nanoでNike+ その2。

昨日代用品で手首装着して調子良かったので、スポーツショップにリストバンドを買いに行ってきました。

とはいえ、やはり売っているのはよくあるタオル地の厚みのあるものばかりで、nanoをクリップするにはちょっと不安な感じ。...ふと見るとワックル・リストというシリコン製のリストバンドが売っていたので、買ってみました。

IMG_0993.JPG

こんなの。2,100円とお高いですが、ゲルマニウム付きなので血行がよくなりそう。

IMG_0995.JPG

早速装着!...どう見ても髪留めのゴムです。

しかし、シリコン製なので汗をかいても滑らず、上の写真のような平たい部分をクリップしているので安定感はバッチリで、走行中も外れそうになることはありませんでした。

これでより快適にNike+を活用できそうです。

6th gen iPod nanoでNike+。

およそ2年前、4th gen iPod nanoNike+を始めましたが、色々あって中断したりして、また再び7月ごろから再開していました。

そして今日、6th gen iPod nano (8GB)が手元に届いたので早速Nike+で使ってみました。

IMG_0991.JPG

4th genと6th gen。パースがついているので分かりづらいけど横幅はほぼ同じ。

IMG_0992.JPG

Nike+のレシーバーをつけて腕に乗せてみたところ。ビデオシーバーみたくなるかと思ったら、結構不格好でした。

4th genは純正のアームバンドで腕に固定していましたが、結構外れやすいので走行中気にしながら使っていました。また画面を見ながら操作できないのもネックでした。

6th genならリストバンドをしてそこにクリップすれば見ながら操作できるし、アームバンドよりも安定して良さそう!と思い、早速実践してみました。

リストバンドが手元にないのでとりあえずスポーツ用の眼鏡ストラップで代用しましたが、がっちりクリップできて安定度は上々、やはり手元で見ながら操作できるのは快適です。

その他、4th genと違った点はこんな感じ。

  • ワークアウトの時間が、1分単位で設定できるようになった(4th genは5分単位)。
  • 一度スワイプする必要はあるが、Power Songボタンが画面に出てくるので分かりやすい。
  • ボタンを押した時の途中経過の読み上げが、ちょっと早口で機械っぽくなった。
  • 今まではあまり気味だったヘッドフォンのコードが逆にほんの少し足りないくらいになった(つける位置が変わったので当たり前だけど)。

とまあ、すこぶるいい感じです。

とりあえずリストバンドと10cmくらいのヘッドフォン延長コードを買ってこようと思います。