examplesフォルダ以下にいくつかのサンプルがある。どれも実際のゲームとは程遠い造りではあるが、その中でも"07.Collision"が最も実ゲームに近いと思われるので、これをベースにするとよい。
キャラクタを動かすための仕組みとして、IrrlichtにはAnimatorの仕組みがある。
しかし、実用的なPCゲームを作るという観点に立ち、敢えてこれを使わないことにする。
移動処理と描画処理を独立して行ないたいのがその主な理由。
IrrlichtのAnimatorを使ってしまうと、キャラクタの移動処理と描画処理を必ず対でする必要がある。これは、フレームレートが落ちた際には移動処理の回数が減ることを意味する。
経過時間によって移動量を変えればよい、と思うかも知れないが、当たり判定処理などは移動毎にせざるを得ず、微妙に結果が変わったり、最悪すり抜けのような現象が発生する可能性もある。例えば、互いに交わる進路上を移動する2つの物体が、同時刻に交差点を通過するケースなどは、接触判定の時間間隔によって異なる結果をとりうる。
特に理由がない限り、画面更新はVSyncに合わせたほうが無難。
VSyncを有効にする場合、createDeviceの引数のvsyncをtrueにする。createDeviceExなら、SIrrlichtCreationParameters::Vsyncをtrueにする。
VSyncが変わってもゲームの進行速度が変動したりしないよう、入力、移動処理等はタイマを見て独立に処理すべき。
Irrlichtには基本的な当たり判定機能が実装されているため、利用することにする。
しかし、07.Collisionのサンプルにおいては、ISceneManager::addCameraSceneNodeFPSによって生成したカメラ兼プレイヤーオブジェクトを使っており、入力から当たり判定までAnimatorによって実装されている。
これは、前述の「移動処理と描画処理を独立して実行する」の実現には不適切であるため、通常のカメラノードを作成して、別途当たり判定をする。
Irrlichtにおける当たり判定処理の基本形は下記の通り。
1.の処理を具体的に記述すると下記のようになる。
TriangleSelectorは基本的にISceneManager::createTriangleSelectorで生成できるが、テレイン(地形)などの巨大メッシュの場合は、ISceneManager::createOctreeTriangleSelectorによって八分木構造を生成することが推奨されている。
scene::IAnimatedMesh* meshTerrain = pSceneManager->getMesh("terrain.x"); // メッシュをロード
scene::IMeshSceneNode* nodeTerrain =
pSceneManager->addOctreeSceneNode(meshTerrain->getMesh(0)); // メッシュをシーンに配置
scene::ITriangleSelector* selTerrain =
pSceneManager->createOctreeTriangleSelector(
nodeTerrain->getMesh(), nodeTerrain); // 当たり判定情報を生成
createOctreeTriangleSelectorは、メッシュデータを与えると八分木構造の当たり判定データを自動生成することが出来るという機能であり、シンプルで使い勝手が良い。全てのデータを厳密に判定すると重くなるので、単純な球体(距離)判定なども併用するとよい。
ここで生成したselTerrainの先には、メッシュデータ”terrain.x”から生成された当たり判定情報が格納されている。
TriangleSelectorを使って、実際に当たり判定をするには下記のようにする。
当たり判定したい物体をnodeXとすると
vector3df currentPos = nodeX->getPosition(); // Xの現在位置
vector3df moveVec = (Xが現在位置からどれだけ移動したいかを示すベクトル);
vector3df colRadius(2, 2, 2); // Xの判定範囲 (この場合、半径2の球体)
core::triangle3df ctri;
vector3df hitPos;
bool outFalling = false;
scene::ISceneNode* pOutNode = NULL;
vector3df posResult = pSceneManager->getSceneCollisionManager()->getCollisionResultPosition(selTerrain,
currentPos, colRadius, moveVec, ctri, hitPos, outFalling, pOutNode);
nodeX->setPosition(posResult); // 実際に移動する
nodeX->updateAbsolutePosition(); // 絶対位置を更新
何かに接触した場合、pOutNodeに接触した物体へのポインタが返る。
接触の有無に関係なく、posResultには当たり判定により修正された位置が返る。何物にも接触しなければ、posResultに入るのは(currentPos + moveVec)である。
Animatorを使わずに自前でsetPositionした場合、最後のupdateAbsolutePositionを呼ばないと、子ノードの位置が正しく更新されない場合があるので注意。
実際のゲームにおいては、通常、複数の物体との当たり判定をする必要がある。この際、全てのTriangleSelectorに対してループ等で順にgetCollisionResultPositionをしてしまうと、複数の物体に同時接触した場合の処理を正しく行なうことができない。
このため、複数の物体の当たり判定をまとめて行なう必要がある。
これを実現するにはMetaTriangleSelectorを使う。これは、下記のように生成する。
scene::ITriangleSelector* selWorld = pSceneManager->createMetaTriangleSelector();
下記のように判定したいTriangleSelectorを追加する。
selWorld->addTriangleSelector(selTerrain);
selWorld->addTriangleSelector(selBuilding1);
selWorld->addTriangleSelector(selBuilding2);
実際に判定する際には、selTerrainの代わりにselWorldを引数にして、前述したのと同様にgetCollisionResultPositionで処理すればよい。
シーンを消去した際には、selWorldはdropにて処分する。
selWorld->drop();
TriangleSelectorは参照カウント管理なので、dropしないと消えない。(メモリリークする)
凹凸のあるテレインを作成し、その上に植林したい場合など、配置地点の高度を算出して、その高さに自動調整できると都合が良い。
こういった場合にも、同様にgetCollisionResultPositionを使用することができる。
特定のX,Zに対する標高Yを算出する関数は下記のようになる。
const float ELEVATION_VERY_LARGE = 10000;
const float ELEVATION_NOHIT = -1e20;
float getElevationFromXZ(irr::IrrlichtDevice* pDevice, scene::ITriangleSelector* pSel, float x, float z) {
vector3df colRadius(.01f, .01f, .01f);
core::triangle3df ctri;
vector3df hitPos;
bool outFalling = false;
scene::ISceneNode* pOutNode = NULL;
vector3df posResult = pDevice->getSceneManager()->getSceneCollisionManager()->getCollisionResultPosition(
pSel, core::vector3df(x, ELEVATION_VERY_LARGE, z),
colRadius, core::vector3df(0, -ELEVATION_VERY_LARGE * 2, 0), ctri, hitPos, outFalling, pOutNode);
return pOutNode ? (pOutNode->getPosition().Y + hitPos.Y) : ELEVATION_NOHIT;
}
pNodeを(x,z)の地点の地表面に配置したいなら、下記のようにする。
float y = getElevationFromXZ(pIrrDevice, selTerrain, x, z);
pNode->setPosition(vector3df(x, y == ELEVATION_NOHIT ? 0 : y, z));
getSceneNodeAndCollisionPointFromRay()を使ったほうが良さそうに思われるかも知れないが、こちらの場合、太さ0の直線との接触判定となる。Irrlichtにおいては、この直線がテレインを構成するポリゴンの境界線上に入った場合に、すり抜けが発生する。このため、確実にどこかにヒットさせたい場合にはお勧めできない。