UIKit iOSアプリへのBox2D物理エンジンの統合

こんにちは



今日は、Box2D物理エンジンを、標準のAppleフレームワークを使用して記述されたゲームアプリケーションに統合することがいかに簡単かを示します。 たとえば、6か月前にスタジオがリリースしたインタラクティブな本です。 この本は子供向けの最初のアプリケーションであり、作業を開始したときはアニメーションの作成経験がほとんどなかったため、強力で十分に文書化された標準のAppleフレームワークを選択しました-当時は簡単でした。 この本は2か月で準備ができました。 ただし、一部のアイデアは実装されていません。 これらの願いから、リストが将来のために残されたので、時間と知識があれば、プロジェクトに戻ります。



物理学



ポイントの1つは物理世界のシミュレーションでした。そのため、ユーザーはオブジェクトを操作したり、オブジェクトを作成したり、投げたり、加速度計を使用して隅から隅まで投げたりすることができました。 この機能を実装するには、物理​​エンジンをプロジェクトに統合する必要がありました。 それで、Cocos2DとBox2Dが新しいプロジェクトでマスターされたとき、合理的な疑問が生じました。もしBox2Dがプログラムのグラフィカルな実装から本質的に独立しているなら、なぜ最初の本でそれを使わないのですか? Webでの短い検索により、素晴らしく簡潔なブログ記事 http://www.cocoanetics.comが作成されました。これは、UIKitの標準アプリケーションでBox2Dを使用する方法を簡単かつ簡単に説明しています。 そして、私たちは仕事に取り掛かりました。 C ++で記述されたエンジンの作業には、現在のプロジェクトコードの大規模な変更が必要になるという事実を最も心配していました。 しかし、幸いなことに、わずかな変更が2〜3回しかかかりません。ページクラスファイルのタイプがObjective-C ++に変更され、コードの簡単な最適化が実行されました。 これらの変更には約4時間かかりました。



動作原理



エンジン統合の原理は単純です(示された記事を参照)。 ページがロードされると、物理的な世界が作成されます。 次に、必要な境界(この場合は画面全体)が割り当てられ、必要なボディが作成されます。 作成されたボディはユーザーには表示されないため、userDataのbodyプロパティに従って、必要なUIKitオブジェクト(UIImageViewクラスの写真など)が割り当てられます。 さらに、viewDidAppearメソッドが呼び出されると、タイマーは必要な頻度で開始し、物理世界のすべての物体の位置を処理して、物体に関連付けられた対応する画像を必要な位置に移動します。 これにより、写真自体が直接衝突するという錯覚が生じます。 viewDidDissapearメソッドが呼び出されると、タイマーが停止し、ユーザーが作成したすべてのボディとそれに対応する画像がワールドとself.viewから削除されます。 この方式では許容可能なフレームレートが得られないという懸念にもかかわらず、iPhone 3Gなどの古いデバイスでも、Cocos2Dベースのアプリケーションでのレンダリングに劣らないという目で、レンダリングが高速であることが判明しました。



本体



元の記事で説明した方法では、ボディの作成に関するメッセージで送信されたビューのサイズに応じて、長方形のボディのみを作成できました。 私たちにとって、これは柔軟な方法ではありませんでした。なぜなら、どんな形の体でも持つことができるからです。 Cocos2Dに基づいたプロジェクトで作業する際に、私たちはMac OS用の便利なプログラムである個々の開発者Bogdan VladuのSpriteHelperを使用しました。 有料のライセンスでは、準備された画像からテクスチャマップを作成し、物理ボディのパラメーター(形状、密度、摩擦係数、弾性など)を設定できます。 -すべてが必要です。 結果のファイルを操作するために、作成者はSpriteHelperLoaderクラスを作成しました。このクラスを使用すると、適切な物理世界で必要なボディとCocos2Dレイヤーを1つのメッセージで作成できます。 1つの問題-このクラスは、Cocos2Dの操作に厳密に焦点を合わせていました。 私はしばらく時間を費やし、そこから「ココナッツ」への言及をすべて「切り捨て」なければなりませんでした。 実際、ボディの1つのパラメーターを取得するためにのみ必要でした-フォーム(この場合、テクスチャマップは使用されません)。 今、私たちの手には、最初の本に物理学を追加し、それらに第二の風を与え、できれば肯定的なレビューを追加するための便利で最も重要な馴染みのある方法がありました。 元の記事から本文を追加する元の方法が書き直されました。

- (void) addPhysicalBodyForView:(UIImageView *)physicalImageView ofType:(NSString *)type { // get image's center coordinates CGPoint position = physicalImageView.center; // Define the dynamic body. b2BodyDef bodyDef; bodyDef.type = b2_dynamicBody; bodyDef.position.Set(position.x/PTM_RATIO, (screenSize.height - position.y)/PTM_RATIO); // convert into Box2D coordinates bodyDef.userData = physicalImageView; // Tell the physics world to create the body b2Body *body = world->CreateBody(&bodyDef); position = CGPointMake(bodyDef.position.x, bodyDef.position.y); // use modified SpriteHelperLoader to get shape tempBody = [bodyLoader bodyWithUniqueName:type atPosition:position world:world]; b2Fixture* fixture = tempBody->GetFixtureList(); b2Shape *shape = fixture->GetShape(); // Define the dynamic body fixture. b2FixtureDef fixtureDef; fixtureDef.shape = shape; fixtureDef.density = fixture->GetDensity(); fixtureDef.friction = fixture->GetFriction(); fixtureDef.restitution = fixture->GetRestitution(); body->CreateFixture(&fixtureDef); // a dynamic body reacts to forces right away body->SetType(b2_dynamicBody); world->DestroyBody(tempBody); fixture = nil; shape = nil; }
      
      







タッチ処理



タッチを処理するために、 組み込みの Box2D メカニズム標準の UIKitタッチ検出方法を使用しました。 メインビューでタッチが登録されると、タッチポイントの座標がUIKit座標系からBox2D座標系に変換されます。 次は、物理的な世界のあらゆる身体に入るためのチェックです。 そして、ヒットがある場合、このボディの間にボディグラウンド(groundBody)との物理的な接続が作成されます。これにより、画面上で指でボディをドラッグできます。 タッチが完了すると、移動中に取得されたインパルスに従って、身体は自由に飛行します(接続を破壊します)。 かなり自然に見えます。 ドラッグしたボディが画面の境界から飛び出さないようにするには、これらのボディ間に作成された接続で衝突の計算を許可する必要があることに注意してください。 これを行うには、collideConnected通信プロパティをYESに設定します。

 class QueryCallback : public b2QueryCallback { public: QueryCallback(const b2Vec2& point) { m_point = point; m_fixture = NULL; } bool ReportFixture(b2Fixture* fixture) { b2Body* body = fixture->GetBody(); if (body->GetType() == b2_dynamicBody) { bool inside = fixture->TestPoint(m_point); if (inside) { m_fixture = fixture; // We are done, terminate the query. return false; } } // Continue the query. return true; } b2Vec2 m_point; b2Fixture* m_fixture; }; [...] #pragma mark - Drag and Drop // source - http://iphonedev.net/2009/08/05/how-to-grab-a-sprite-with-cocos2d-and-box2d/ - (void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { UITouch* myTouch = [touches anyObject]; CGPoint location = [myTouch locationInView: [myTouch view]]; location = CGPointMake(location.x, screenSize.height-location.y); m_mouseWorld.Set(location.x/PTM_RATIO, location.y/PTM_RATIO); if (m_mouseJoint != NULL) { NSLog(@"m_mouseJoint != NULL"); return; } b2AABB aabb; b2Vec2 d; d.Set(0.001f, 0.001f); aabb.lowerBound = m_mouseWorld - d; aabb.upperBound = m_mouseWorld + d; // Query the world for overlapping shapes. QueryCallback callback(m_mouseWorld); world->QueryAABB(&callback, aabb); b2Body* nbody = NULL; if (callback.m_fixture) { nbody = callback.m_fixture->GetBody(); } if (nbody) { b2MouseJointDef md; md.bodyA = groundBody; // md.bodyB = nbody; md.target = m_mouseWorld; md.collideConnected = YES; #ifdef TARGET_FLOAT32_IS_FIXED md.maxForce = (nbody->GetMass() < 16.0)? (1000.0f * nbody->GetMass()) : float32(16000.0); #else md.maxForce = 1000.0f * nbody->GetMass(); #endif m_mouseJoint = (b2MouseJoint*)world->CreateJoint(&md); nbody->SetAwake(YES); } } - (void) touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { UITouch* myTouch = [touches anyObject]; CGPoint location = [myTouch locationInView: [myTouch view]]; // translate uikit coordinates into box2d coordinates location = CGPointMake(location.x, screenSize.height-location.y); m_mouseWorld.Set(location.x/PTM_RATIO, location.y/PTM_RATIO); if (m_mouseJoint) { m_mouseJoint->SetTarget(m_mouseWorld); } } - (void) touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { NSLog(@"touches ended"); if (m_mouseJoint) { AudioServicesPlaySystemSound (soundThrust); world->DestroyJoint(m_mouseJoint); m_mouseJoint = NULL; } } - (void) touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event { [self touchesEnded:touches withEvent:event]; }
      
      







加速度計



オブジェクトでリアルなボックスを作成しようとすると、重力を忘れてはなりません。いくつかの落とし穴があります。 もちろん、元の記事で行ったように、単純なパスをたどって、加速度ベクトルの読み取り値から重力ベクトルの成分を取得し、XとYの2つの軸に沿って値を取得できます。

 - (void)accelerometer:(UIAccelerometer *)accelerometer didAccelerate:(UIAcceleration *)acceleration { b2Vec2 gravity; gravity.Set( acceleration.x * 9.81, acceleration.y * 9.81 ); world->SetGravity(gravity); }
      
      





しかし、この方法では、デバイスが水平面にある場合、デバイスのX軸とY軸上の地球の重力の投影がほぼゼロになるため、誤って望ましくない無重力状態を作り出すことができます。 このような仮想無重力状態の場合、オブジェクトは空中に不自然に舞い上がります。 重力ベクトルが常に1であり、オブジェクトがデバイスの位置に関係なく一定の力の作用下にある場合、膝やテーブルの上、手に垂直に、または一般的に画面を下にした方が自然です。 重力の方向を計算するために、このやや複雑なアルゴリズムを実装しましたが、小さなノウハウとして秘密にしておきます。 また、重力ベクトルをより正確に定義し、新しいデバイスでより正確に制御するには、加速度計を使用できます。UIAccelerometerDelegateプロトコル(iOS 5では使用する価値がなくなった)を使用するだけで、CoreMotionフレームワークから統合されたCMMotionManagerに移動する必要があります。



結果のビデオデモ







結論の代わりに



物理エンジンを本の4ページに統合するのに数日しかかかりませんでした。各ページの小さなゲームプロットの作成、適切なグラフィックスとコーディングの作成など、合計コストは小さく、多くの新機能があります。 さあ、あなた!

ご清聴ありがとうございました。



All Articles