第十章 世界類(world class)
簡介
b2World類包含物體(body)和關節(joint),它負責模拟工作的各個方面并且支援異步的查詢(例如AABB查詢和射線投射等等)。Box2D中的大部分互動都和b2World對象有關。
構造和析構世界對象
構造一個世界對象相當簡單,你隻需要提供一個重力向量和一個布爾值用來指明是否允許物體進入休眠狀态。通常你通過new和delete方法來構造和析構世界。
b2World*myWorld = new b2World(gravity, doSleep);
… do stuff …
deletemyWorld;
使用世界對象
世界類包含很多用來構造和析構物體和關節的工廠,這些工廠後面關于物體和關節的小節中我們會進一步讨論,現在我們先來說一下b2World中一些其他的互動元素。
模拟
世界類被用來驅動模拟,通過制定一個時間間隔(time step)和兩個疊代次數(速度疊代和位置疊代,我們前面章節中做過介紹),例如:
float32timeStep = 1.0f / 60.0f;
int32velocityIterations = 10;
int32positionIterations = 8;
myWorld->Step(timeStep,velocityIterations, positionIterations);
在這個時間間隔模拟完成之後,你可以檢查物體和關節的資訊,通常你會需要擷取物體的位置,以便更新和渲染你的角色。你可以在你遊戲循環的任何位置去模拟執行時間間隔,但是你應該注意事件執行的順序。例如,如果你想得到新的物體在那一幀的碰撞結果,你就必須在執行前先建立物體。
正如我們在前面HelloWorld那個例子中讨論的,你應該使用固定的時間間隔,通過使用更大的時間間隔,你可以提高執行效率,但是會以降低幀頻率為代價。大體上你的時間間隔不應該高于1/30秒,如果時間間隔為1/60秒,那麼模拟效果将非常好。
疊代次數決定了限制解析器每一次完全解析要對世界中所有的接觸和關節完成多少次疊代。疊代次數越多,效果越好,但是不要以減小時間間隔為代價增加疊代次數,舉個例子,60Hz(時間間隔)和10次疊代的組合要遠比30Hz和20次疊代的組合要好。
一個時間間隔結束後,你應該清理掉任何你施加在物體上的力,你可以通過調用b2World::ClearForces方法來完成清理。這樣你就可以在多個子時間間隔内應用相同的力的作用域了。
暢遊世界(exploring the world)
世界時物體,接觸和關節的容器,你可以擷取一個物體,接觸,以及關節的清單并周遊他們。例如,下面的代碼喚醒世界中所有的物體:
for (b2Body* b= myWorld->GetBodyList(); b; b = b->GetNext())
{
b->SetAwake(true);
}
不幸的是,真正的程式可能遠比這個要複雜。比如下面這段出錯的代碼:
for (b2Body* b= myWorld->GetBodyList(); b; b = b->GetNext())
{
GameActor* myActor = (GameActor*)b->GetUserData();
if (myActor->IsDead())
{
myWorld->DestroyBody(b); //ERROR: now GetNext returns garbage.
}
}
程式正常執行,直到物體被析構掉了。當物體析構了之後,指向下一個物體的指針就失效了,是以下一次循環去調用b2Body::GetNext()方法的時候,傳回的是沒有用的資訊。這個問題的解決方法是在析構物體之前,儲存指向下個物體的指針:
b2Body* node =myWorld->GetBodyList();
while (node)
{
b2Body* b = node;
node = node->GetNext();
GameActor* myActor = (GameActor*)b->GetUserData();
if (myActor->IsDead())
{
myWorld->DestroyBody(b);
}
}
這樣就可以安全地析構掉目前的物體了,然而,你可能在遊戲中需要調用某個方法來“處理掉”多個物體,這時你就要非常小心了,解決方法依具體應用而異,但是為了友善起見,我會給出一種解決方法:
b2Body* node =myWorld->GetBodyList();
while (node)
{
b2Body* b = node;
node = node->GetNext();
GameActor* myActor = (GameActor*)b->GetUserData();
if (myActor-IsDead())
{
bool otherBodiesDestroyed= GameCrazyBodyDestroyer(b);
if (otherBodiesDestroyed)
{
node =myWorld->GetBodyList();
}
}
}
很顯然如果想要這個方法能夠正常工作,GameCrazyBodyDestroyer函數必須要誠實地記錄那些物體被析構掉了。
AABB查詢(AABBqueries)
又是如果你想要得到某個區域中的所有形狀,b2World類中提供一個複雜度為log(N)的方法,使用broad-phase資料結構來實作。你需要提供一個世界坐标系下的AABB,并且需要實作b2QueryCallback類,接着每個和你提供的AABB(查詢用的AABB)重合的裝置,世界對象就會調用你的類來處理,要繼續查詢就傳回true,否則傳回false。例如,下面的代碼查找所有和指定的AABB相交的裝置,接着喚醒這些裝置對應的物體。
classMyQueryCallback: public b2QueryCallback
{
public:
bool ReportFixture(b2Fixture* fixture)
{
b2Body* body =fixture->GetBody();
body->SetAwake(true);
// Return true to continuethe query.
return true;
}
};
…
MyQueryCallbackcallback;
b2AABB aabb;
aabb.lowerBound.Set(-1.0f,-1.0f);
aabb.upperBound.Set(1.0f,1.0f);
myWorld->Query(&callback,aabb);
回調函數的執行順序不是固定的,是以不要基于這個假設來寫邏輯。
射線投射(ray cast)
你可以使用射線投射來做視線(line-of-sight)檢查,發射炮彈等等。通過實作一個回調類并提供起點和終點,你就可以執行一次射線投射了。每當有一個裝置被提供的射線擊中的時候,世界類就會調用你的回調類來處理,它會将這些參數傳入你的回調方法中:裝置,入射點,機關法向量和射線從起點到入射點所經過的距離與射線長(注:射線當然無線長了,這裡長度是指起點到終點的距離)的比值(分數)。同樣,這個回調函數的執行順序是不固定的,不要做任何錯誤的假設。
你可以通過傳回的分數(fraction)來控制射線投射是否繼續執行,傳回0表示停止執行,傳回1表示投射繼續執行,就如同還沒有擊中任何物體一樣。如果你傳回參數清單中傳入的分數(比值),那麼射線會在入射點處被切斷。綜上所述,你可以投射任何形狀,投射所有形狀,或者通過傳回适當的分數來投射離得最近的形狀。
你也可以傳回-1來過濾掉這個裝置,這時投射會繼續執行,就好像這個裝置根本不存在一樣。
下面是一個例子:
// This classcaptures the closest hit shape.
classMyRayCastCallback: public b2RayCastCallback
{
public:
MyRayCastCallback()
{
m_fixture = NULL;
}
float32 ReportFixture(b2Fixture* fixture, const b2Vec2& point,const b2Vec2& normal, float32 fraction)
{
m_fixture = fixture;
m_point = point;
m_normal = normal;
m_fraction = fraction;
return fraction;
}
b2Fixture* m_fixture;
b2Vec2 m_point;
b2Vec2 m_normal;
float32 m_fraction;
};
MyRayCastCallbackcallback;
b2Vec2point1(-1.0f, 0.0f);
b2Vec2point2(3.0f, 1.0f);
myWorld->RayCast(&callback,point1, point2);
注意:由于取近似值的誤差(round-offerror),射線有可能從靜态環境中多邊形之間的細小裂縫穿出(漏過),如果對于你的應用程式來說這個結果不理想,那麼你可以略微增大你的多邊形的尺寸來修複這個問題。
力和沖量
你可以對物體施加力、扭矩和沖量。當你施加力或者沖量時,需要提供一個世界坐标作為力或沖量的作用點,這通常會導緻在質心附近行程一個扭矩。
voidApplyForce(const b2Vec2& force, const b2Vec2& point);
voidApplyTorque(float32 torque);
voidApplyLinearImpulse(const b2Vec2& impulse, const b2Vec2& point);
voidApplyAngularImpulse(float32 impulse);
對物體施加力、扭矩或沖量會喚醒物體,有時候這是我們不希望的。例如,你可能對一個物體施加一個恒定的力,并且允許物體休眠來提高效率,這是你可以使用下面的方法:
if(myBody->IsAwake() == true)
{
myBody->ApplyForce(myForce, myPoint);
}
坐标變換(coordinate transformation)
物體類有一些實用的方法用來幫助你對點和向量的坐标進行變換(世界坐标和本地坐标轉換)。如果你不了解這些概念,請參閱Jim Van Verth和Lars Bishop所著的《遊戲與互動應用的數學基礎
》(《Essential Mathematics for Games andInteractive Applications》)一書。下面這些方法執行效率很高(作為内聯函數使用時):
b2Vec2 GetWorldPoint(const b2Vec2& localPoint);
b2Vec2 GetWorldVector(const b2Vec2& localVector);
b2Vec2 GetLocalPoint(const b2Vec2& worldPoint);
b2Vec2 GetLocalVector(const b2Vec2& worldVector);
清單(list)
你可以周遊一個物體上所有的裝置,這個功能主要是用來需要擷取裝置的使用者資料(userdata)的:
for (b2Fixture* f = body->GetFixtureList(); f; f =f->GetNext())
{
MyFixtureData* data =(MyFixtureData*)f->GetUserData();
… do something with data …
}
同理,你可以周遊物體的關節清單。
物體類同樣提供一個所有相關的接觸(contact)的清單,你可以通過這個清單來擷取目前的接觸資料。要小心的是,這個清單中不會包括隻在前一個時間間隔記憶體在的那些接觸。