blog
cocos3d+Bulletで物理シミュレーション (実装編) #cocos2d_2011_adcal
@yoichinejiさん主催のcocos2d Advent Calendar 2011、1日遅れで19日目の記事です。
前日の記事: ゲームを作るなら効果音、声、BGMも必要だよね (@aoi68kさん)
前回導入編の続きで、実装編です。
サンプルコードは前回と同じものです。(こちらからダウンロードできます)
固定された床にキューブが降り注ぎ、積み重なっていくデモです。
3Dモデルの読み込みと表示
cocos3dでの3Dモデルの読み込み・表示は、テンプレートから作成したプロジェクトのCC3World派生クラスにもある通り、とても簡単です。
[self addContentFromPODResourceFile: @"hello-world.pod"];
これだけ。
モデルのフォーマットはPOD(PowerVR Object Data)ですが、Collada2PODというツールでCOLLADAフォーマット(.dae)から変換できます。
大概のモデリングツールはCOLLADAで出力できると思います。
Collada2PODのダウンロードはこちらから(要サインアップ)。
Collada2PODの設定ファイル(Collada2PODSettings.txt)がcocos3dのToolsフォルダに同梱されているので、これをLoad Optionsから読み込んで変換するとうまい具合にいきます。
テクスチャを貼っている場合は、その画像をプロジェクトのリソースとして追加すればOKです。
物理シミュレーションの準備
物理シミュレーションの実装を記述するソースコードでbtBulletDynamicsCommon.hをimportします。
#import "btBulletDynamicsCommon.h"
BulletはC++で記述されているため、Objective-Cから使う場合はObjective-C++で記述します。
該当するソースコードの拡張子を「.m」から「.mm」に変更します。
サンプルコードではcocos2dAdventCalendarDay16World.hでbtBulletDynamicsCommon.hをimportしているため、このヘッダファイルをimportしているすべてのファイルの拡張子が「.mm」になっていますが、実際にC++のコードを記述しているのはcocos2dAdventCalendarDay16World.mmだけです。
ダイナミクスワールドの作成
まず物理シミュレーションを行うための世界・ダイナミクス(力学)ワールドを作成します。
今回は単純な剛体による物理シミュレーションのため、btDiscreteDynamicsWorldクラスを使います。
collisionConfiguration = new btDefaultCollisionConfiguration();
collisionDispatcher = new btCollisionDispatcher(collisionConfiguration);
broadphaseInterface = new btDbvtBroadphase();
constraintSolver = new btSequentialImpulseConstraintSolver();
dynamicsWorld = new btDiscreteDynamicsWorld(collisionDispatcher, broadphaseInterface, constraintSolver, collisionConfiguration);
dynamicsWorld->setGravity(btVector3(0.0f, -9.8f, 0.0f));
最後にY軸のマイナス方向に1Gの重力を設定しています。
コリジョンシェイプの作成
コリジョンシェイプとは衝突判定を行うために物体がどんな形状をしているかを表すオブジェクトです。
btCollisionShapeクラスを基底として、直方体、球、円柱、円錐、カプセル型などそれぞれの形状の派生クラスが用意されています。
床用のコリジョンシェイプ(直方体)の作成はこんな感じです。
groundCollisionShape = new btBoxShape(btVector3(4.0f, 0.25f, 4.0f));
コンストラクタの引数にX、Y、Z方向の大きさを渡して作成します。
Bulletでは大きさを表すときによくhalf extent(半分の大きさ)が使われています。なので、このシェイプの大きさも縦横8×8、高さ0.5となります。
コリジョンシェイプは使い回すことができます。
サンプルコードでも降り注ぐキューブの方は、一つのコリジョンシェイプをインスタンス変数として保持しておいて使い回しています。
モーションステートの作成
モーションステートとはBulletによる物理シミュレーションの結果変化した向きや位置を受け渡すためのインターフェースです。
通常はbtMotionStateの派生クラスを自分で作る必要はないようなので、デフォルトのbtDefaultMotionStateクラスを使います。
btDefaultMotionState *groundMotionState = new btDefaultMotionState(btTransform(btQuaternion(0.0f, 0.0f, 0.0f, -1.0f),
btVector3(groundMeshNode.location.x, groundMeshNode.location.y, groundMeshNode.location.z)));
コンストラクタに初期状態の回転・位置を示す変換情報を渡して作成します。位置は配置済みのCC3MeshNodeオブジェクトのlocationに合わせています。
回転を示すクオータニオンのWが-1な理由は後ほど。
リジッドボディの作成
リジッドボディ(剛体)とはBulletにより物理シミュレーションを行う物体を表すオブジェクトです。
先に作成しておいたコリジョンシェイプとモーションステートをリジッドボディに関連付けます。
btRigidBody::btRigidBodyConstructionInfo groundRigidBodyCI(0.0f, groundMotionState, groundCollisionShape, btVector3(0.0f, 0.0f, 0.0f));
btRigidBody *groundRigidBody = new btRigidBody(groundRigidBodyCI);
btRigidBodyConstructionInfo構造体で、物体の質量、モーションステート、コリジョンシェイプ、慣性を設定します。
Bulletには、シミュレーションによって動かされる「動的剛体」、衝突するが動かない「静的剛体」、ユーザーによって動かされ動的剛体を押すことはできるがそれ自体は影響を受けない「キネマティック剛体」の3種類があります。
質量を正の値にすると動的剛体、0にすると静的剛体になります。静的剛体にキネマティックフラグを設定するとキネマティック剛体になります。
床は衝突するが動かない静的剛体なので、質量に0を指定しています。
降り注ぐキューブは動的剛体なので、以下のようなコードになります。
GLfloat mass = 1.0f;
btVector3 localInertia(0.0f, 0.0f, 0.0f);
cubeCollisionShape->calculateLocalInertia(mass, localInertia);
// (中略)
btRigidBody::btRigidBodyConstructionInfo rigidBodyCI(mass, motionState, cubeCollisionShape, localInertia);
btRigidBody *rigidBody = new btRigidBody(rigidBodyCI);
動的剛体の場合は、慣性をcalculateLocalInertia()関数によって算出します。
こうして作成したリジッドボディには、パラメータで表面の摩擦や弾み具合など物体の材質を設定できます。
このリジッドボディをダイナミクスワールドに追加することによって物理シミュレーションされるようになります。
dynamicsWorld->addRigidBody(rigidBody);
シミュレーションの実行
まずCCSchedulerを使って、更新用メソッドが毎フレーム呼ばれるようにしておきます。
[[CCScheduler sharedScheduler] scheduleSelector:@selector(updatePhysics:) forTarget:self interval:0 paused:NO];
更新用メソッドの方では、dynamicsWorldのstepSimulation()関数を呼びます。
dynamicsWorld->stepSimulation(dt, 2);
これで物理シミュレーションが1ステップ実行され、各物体の向きや位置が更新されます。
あとは、各物体のモーションステートから変換情報を取り出して、対応するCC3Nodeオブジェクトに反映していけばいいわけです。
CC3MeshNode *meshNode = cube.meshNode;
btRigidBody *rigidBody = cube.rigidBody;
btTransform transform;
rigidBody->getMotionState()->getWorldTransform(transform);
btVector3 pos = transform.getOrigin();
meshNode.location = cc3v(pos.getX(), pos.getY(), pos.getZ());
btQuaternion quaternion = transform.getRotation();
meshNode.quaternion = CC3Vector4Make(quaternion.getX(), quaternion.getY(), quaternion.getZ(), -quaternion.getW());
物体の位置は座標をそのまま渡せばOKです。
回転は、クオータニオンのWを-1倍して渡します。回転方向が合わず物体同士がめり込むことがあったのでこうしています。
完成したデモはこんな感じです。
ちょうど0.6.5がリリースされたcocos3dですが、3Dオブジェクトを簡単に扱え、かつ、CCActionによるアクションももちろん使えるという非常に便利なフレームワークです。
ぜひ使ってみてください!
参考資料:
cocos3d APIリファレンス
http://brenwill.com/docs/cocos3d/0.6.5/api
cocos3d プログラミングガイド
http://brenwill.com/2011/cocos3d-programming-guide/
Bullet APIリファレンス
http://bulletphysics.com/Bullet/BulletFull/
Bullet 日本語マニュアル
http://bulletjpn.web.fc2.com/
次の記事: Cocos3dをさわってみた。 (@kclabさん)
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」を選択します。
プロジェクト名を付けて適当な場所に保存します。
これをそのままビルド・実行してみるとHello worldの文字が回転します。
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」を選択します。
Product Nameは「Bullet」、Typeは「Static」にして、Use Automatic Reference Countingはオフにしておきます。
Xcodeのメニュー File → Add Files to "<プロジェクト名>"... を選択して、Bulletのソースファイルをプロジェクトに追加します。
このときAdd to targetsで「Bullet」にチェックして、先ほどのBulletフォルダを指定します。
Project Navigatorでビルドしないファイルの参照をごそっと削除します。
プロジェクトの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を追加。
さて、ようやくこれでコードを書く準備ができました。
思いのほか長くなってしまったのでここまでを導入編として、実際の物理シミュレーションの実装は次の機会にしたいと思います。
次の記事: よく分かってない人が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の画像に設定しても文字の方は半透明にならないので、ちょっと浮いてしまいます。
そこで、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:を呼べばいい、という寸法です。
長押しボタン
指をなるべく動かさず素早くボタンの機能を切り替える、というようなことが必要なシチュエーションがあったので、長押しボタンを実装してみました。
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を使ってボタンを長押しするともうひとつのボタンとトグルで切り替わるようにしてみました。
さて、cocos2d Advent Calendar 2011もここから2周目に入りそうです。
まだまだ参加枠はあるようなので小粋なTipsをお持ちの方、ぜひ参加してみませんか?
次の記事: cocos2dキャラクタークラス設計の考察 (@ajinotatakiさん)