Bullet Physicsで簡単なゲームを作ってみる / ゲーム制作に必要な機能 / C++ Windows
※ブログで公開していた記事です。情報が古いかもしれません。
物理エンジンといえば、建物を破壊したりリアルな挙動を実現する派手なゲームを思い浮かべますが、その様なゲームでなくても物理エンジンを活用可能です。
物理エンジンには形状同士の衝突を検出する機能があり、多数の形状同士の衝突判定を高速に行うことができます。キャラクターの移動、攻撃判定、イベントトリガなどを物理エンジンを利用すれば簡単かつ高速に判定することができます。
使い方に少しコツがいるためにゲーム制作に必要な機能を説明しながら簡単な3Dゲームを作ってみます。
ゲームでの物理エンジン
ゲームでの利用には主に3種類、物理エフェクト、物理シミュレーション、コリジョン判定などがあります。
物理エフェクト
髪の毛や服を揺らす、破壊したオブジェクトの破片の挙動など、見た目だけでゲームシステムには影響しない処理です。ゲームで映像のクオリティーを上げるためによく使われています。
今回作成するゲームでは利用しません。
物理シミュレーション
物理挙動をゲームシステムに組み込んだゲームでの利用方法です。ゲームの挙動は物理法則を無視したものが多いため、使う機会はそれほどありません。
今回作成するゲームでは物体の物理挙動を物理エンジンの剛体を使って実現します。
コリジョン判定(衝突判定)
見た目が派手な物理エフェクトに目が行きがちですが、たぶん物理エンジンで最も利用されている機能だと思います。剛体などの衝突を検出する処理ですが、この機能だけでも利用できます。複雑な場面に対応できるよう設計されているため、高機能で高速です。キャラクターの移動、攻撃判定、イベントトリガなど応用範囲は広いです。
ゲーム舞台の準備(Bulletで利用できる部品)
ゲームの世界を構成する部品について簡単に説明していきます。
Bulletの入手はここから
ビルドについては、この記事を
コリジョン形状 btCollisionShape
様々な形状のコリジョンを作成することができ、この形状を使って剛体の運動(衝突)やゲーム用判定を行います。Bulletで利用できる形状は、剛体のような運動(移動)することができる凸面(convex)と、動けないが任意の形状を表現できる凹面(concave)の2つに分けられます。
計算式で表現される凸面(convex)形状
球 btSphereShape
カプセル btCapsuleShape
円柱 btCylinderShape
円錐 btConeShape
この4つは半径と高さの2つのパラメータで表現される形状で、完全な曲面を表現できます。情報量が少なく、計算が簡単、転がる物体を表現可能です。ゲームの判定処理での使用頻度が高い形状だと思います。
頂点データで表現される形状(凸面多面体)
線/三角形/三角錐 btBU_Simplex1to4
BOX btBoxShape
凸包(convex) btConvexHullShape
複数の頂点データで表現される形状です。btConvexHullShapeですべての凸面の多面体を表現可能です。
ですが計算の効率化のため、各頂点を直角に限定したbtBoxShape、4頂点以下の形状(点、線、三角形、三角錐)を表現するbtBU_Simplex1to4などがあり、できるだけ効率化した形状を使用すれば計算負荷が軽くなります。
凸面三角形メッシュ(btConvexTriangleMeshShape)
三角形ポリゴンで凸面の多面体を表現します。へこんだり、穴の開いた形状を指定できますが、上図のように凸面の多面体の内部にある頂点(三角形)は無視されます(凸包)。
ポリゴン数が多いと計算負荷が高くなるため、どうしても形状が表現できないときの最終手段です。
凹面三角形メッシュ(btBvhTriangleMeshShape)
任意の形状を表現できますが、剛体など移動するオブジェクトには使用できません。地面や複雑な建物のコリジョンなどで利用されることが多いと思います。凸面の形状と違い、三角形ポリゴンで構成されたハリボテで簡単にコリジョン抜けが発生してしまうため、利用には注意が必要です。凸面形状は内部に侵入しても外に押し出されます。
形状の組み合わせ(btCompoundShape)
移動するオブジェクトには凸面しか使用できませんが、凸面の形状を複数組み合わせることで、任意の形状を表現可能です。ただし、複雑にし過ぎると物理計算が不安定になります。ご利用は計画的に。
Bulletでの作成(C++)
各形状を作成するソースコードです。三角形メッシュの形状を作成する際は注意が必要です。コンストラクタに渡すメッシュ管理クラスのポインタを自分で保持し、利用が終わったら自分で削除する必要があります。
Bulletはユーザーが設定するポインタの削除を実行しないので注意。自分で参照管理、リソース管理が必要です。
//BOX
btBoxShape* shape = new btBoxShape(btVector3(w/2,h/2,d/2));
// 球
btSphereShape* shape = new btSphereShape(radius);//半径
// カプセル 球+円柱+球
btCapsuleShape* shape = new btCapsuleShape(radius,height);//半径,円柱高さ
// 円柱
btCylinderShape* shape
= new btCylinderShape(btVector3(radius, height, radius));//半径,高さ
// 円錐
btConeShape* shape = new btConeShape(radius, height);//半径,高さ
// 三角錐
btVector3 vertex[4] ={
{ 0,0,1.5f },{ -1.5f,0,-1.5f },{ 1.5f,0,-1.5f },{ 0,2,0 },
};
btBU_Simplex1to4* shape
= new btBU_Simplex1to4(vertex[0], vertex[1], vertex[2], vertex[3]);
// 三角形
btBU_Simplex1to4* shape = new btBU_Simplex1to4(vertex[0], vertex[1], vertex[2]);
// 線
btBU_Simplex1to4* shape = new btBU_Simplex1to4(vertex[0], vertex[1]);
// 点
btBU_Simplex1to4* shape = new btBU_Simplex1to4(vertex[0]);
// convex hull 凸包
btScalar vertex[] = { Vx1,Vy1,Vz1,,,,};
btConvexHullShape* shape
= new btConvexHullShape( (btScalar*)vertex, vertex_num, sizeof(btScalar)*3);
//convex triangle 凸面三角形
//メッシュ情報を記録するクラス
btTriangleMesh* triangle_mesh = new btTriangleMesh();
// 頂点登録
btScalar vertex[] = { Vx1,Vy1,Vz1,,,,};
int index[] = {0,1,2, ,,,,, }; //三角形リスト
for(int i=0; i<vertex_num; ++i){
const btScalar* vf = &vertex[i*3];
btVector3 v(vf[0], vf[1], vf[2]);
triangle_mesh->findOrAddVertex(v, false);
}
// Index登録
for(int i=0; i<index_num; i+=3){
triangle_mesh->addTriangleIndices(index[i], index[i+1], index[i+2]);
}
//引数で渡すbtTriangleMesh*は保持しておく、内部でコピーさ入れない
// Bullet内部処理で削除されないため、使用後は自分でdelete
btCollisionShape* shape = new btConvexTriangleMeshShape(triangle_mesh);
//concave triangle 凹面三角形
//メッシュ情報を記録するクラス
btTriangleMesh* triangle_mesh = new btTriangleMesh();
(略)
btBvhTriangleMeshShape* shape = new btBvhTriangleMeshShape(triangle_mesh);
// 形状の組み合わせ
btCylinderShape* board_shape = new btCylinderShape(btVector3(10, 1, 10));
btCylinderShape* leg_shape = new btCylinderShape(btVector3(1, 2, 1));
// 形状の組み合わせ
btCompoundShape* comp_shape = new btCompoundShape;
// ローカル姿勢を指定してbtCompoundShapeに追加
btTransform local = btTransform::getIdentity();
local.setOrigin(btVector3(0, 2, 0));
comp_shape->addChildShape(local, board_shape);
local.setOrigin(btVector3(-5, 0, -5));
comp_shape->addChildShape(local, leg_shape);
local.setOrigin(btVector3(5, 0, 5));
comp_shape->addChildShape(local, leg_shape);
local.setOrigin(btVector3(5, 0, -5));
comp_shape->addChildShape(local, leg_shape);
local.setOrigin(btVector3(-5, 0, 5));
comp_shape->addChildShape(local, leg_shape);
コリジョンオブジェクト btCollisionObject
位置情報とコリジョン形状を使って、物理ワールドでのコリジョン判定を行う実体(オブジェクト)です。剛体(btRigidBody)だけでなく、判定処理専用のゴースト(btGhostObject)というオブジェクトがあります。
剛体(btRigidBody)
剛体を物理法則に従って移動させるコリジョンオブジェクトです。特に説明は不要だと思います。質量を0にすると移動しない完全に静止した剛体になります。
btCollisionShape* shape = 剛体の形状;
btScalar mass = 質量;
// 移動するオブジェクトかどうか
bool is_dynamic = (mass != 0.0f);
// 慣性モーメント
btVector3 inertia(0, 0, 0);
if(is_dynamic) {
shape->calculateLocalInertia(mass, inertia);
}
// 剛体操作
// 剛体位置の設定/取得など
btDefaultMotionState* mot = new btDefaultMotionState(transform);
// 剛体作成
btRigidBody::btRigidBodyConstructionInfo rb_info(mass, mot, shape, inertia);
btRigidBody* rigid = new btRigidBody(rb_info);
// 物理ワールドに追加
world->addRigidBody(rigid);
// 設定したポインタはBulletが削除しないため、使用後自分でdelete
ゴースト(btGhostObject)
剛体などの他のコリジョンオブジェクトから影響を受けず、衝突状態だけを検出するオブジェクトです。衝突情報を使用しキャラクターの移動など各種判定を行います。ゲームプログラムでの主役といっていいと思います。
設定によって、剛体を跳ね返すか通過させるか選べます。跳ね返す場合、静止した(質量0)剛体とほぼ同じ挙動ですが、ぶつかった剛体を検出することができます。さらに詳しい衝突状態を効率よく取得できるbtPairCachingGhostObjectという派生クラスもあります。
btCollisionShape* shape = ゴーストの形状;
btGhostObject* ghost = new btGhostObject;
// 形状設定
ghost->setCollisionShape(shape);
// 他のオブジェクトへの反応なし
// CF_NO_CONTACT_RESPONSEを指定しないと剛体と衝突する
ghost->setCollisionFlags( ghost->getCollisionFlags()
| btCollisionObject::CF_NO_CONTACT_RESPONSE);
// 位置設定
btTransform t(rot0, btVector3(0, 30, 25));
ghost->setWorldTransform(t);
// 物理ワールドに追加
world->addCollisionObject(torushole_ghost);
// 設定したポインタはBulletが削除しないため、使用後自分でdelete
拘束(Constraint)
剛体と剛体を接続する機能です。ばねや蝶番(ヒンジ)などいろいろな種類が用意されています。ゲーム処理自体ではあまり使用することはないと思いますが、物理エフェクトでよく利用されます。※個人の感想です。
//2点間接続(ボールジョイント)
btPoint2PointConstraint* constraint = new btPoint2PointConstraint(
rigid_a, rigid_b, btVector3(0, 51, 10), btVector3(0, -1, 10));
world->addConstraint(constraint);
アクションインターフェイス(btActionInterface)
stepSimuration関数内から呼ばれるコールバック(仮想関数)を定義するクラスで、主にゴーストを使った判定を行うためのクラスです。ゴーストを使った判定をBulletのstepSimuration関数外でも実行できますが、btActionInterfaceを使用したほうが、より正確な判定処理や結果の反映が可能です。
//ゴーストを利用した判定処理
class MyAction : public btActionInterface
{
public:
//Bulletから呼び出される関数
virtual void updateAction(btCollisionWorld* collisionWorld, btScalar deltaTimeStep)
{
if(!pGhost)return;
int num = pGhost->getNumOverlappingObjects();
for(int i=0; i<num; ++i){
// 重なっているコリジョンオブジェクト(剛体など)
btCollisionObject* obj = pGhost->getOverlappingObject(i);
// 判定処理など
}
}
virtual void debugDraw(btIDebugDraw* debugDrawer)
{
// デバッグ描画
}
private:
btGhostObject* pGhost;//判定用ゴースト
};
//world->addAction()で追加
//stepSimuration関数内部からupdateAction関数が呼ばれる
ゲーム(ちゃぶ台返し)の作成
これまで紹介した機能をすべて使って、簡単なゲームを作ってみました。マウスドラッグでちゃぶ台返しをひっくり返し、上に乗ったものが空中にある輪っかを通過したらポイント獲得、という簡単なゲームです。
輪っか通過の判定にゴーストとアクションインターフェイスを利用しています。輪っかにゴースト(球)を配置、重なった剛体の動きを監視して通過したらポイント加算と輪っかを回転させています。
ソースコード(VS2022プロジェクト)
Bullet3.24同梱。すぐにビルド、実行できます。
Bulletを使ったゲーム作成の基礎が詰まったソースコードになっています。
表示処理はDirectX11で実装、実験用の簡単なレンダリング処理です。
以前ブログで公開していたソースコードがひどすぎたので作り直してます。
なるべくわかりやすく
リソース管理やマルチスレッド関係の処理を削除
64bit (x64)対応
bullet計算精度をdouble
など
必要な環境
Visual Studio 2022(C++)
Windows10(64bit)
DirectX11
利用しているオープンソースとライセンス
Bullet3.24 (zlib)
DirectXTex (MIT)
DirectXTK (MIT)
ダウンロードの際、ブラウザからブロック(危険ファイル)されるかもしれませんが、危険なファイルではありません。念のため、使用する前に内容の確認をお願いします。
ダウンロード(ZIP)
ここから先は
¥ 400
この記事が役に立ったという方は、サポートお願いします。今後の製作の励みになります。