Oh!Coder

Coding Life

第十章 世界类

| Comments

声明:此文章翻译自Box2D v2.2.0用户手册,仅供学习参考。

  • 关于(About)

b2World类包含了物体和连接器。它管理了模拟相关的所有方面并且允许异步需求(像AABB需求和光线投射(ray-casts))。Box2D中的大部分交互将会使用b2World对象。

  • 创建并销毁一个世界(Creatng and Destroying a World)

创建一个世界相当简单。你只要提供一个重力向量和一个是否允许物体有睡眠状态的布尔值(Boolean)。通常你会使用new和delete关键字来创建和销毁一个世界。

1
2
3
b2World* myWorld = new b2World(gravity, doSleep);
...do stuff...
delete myWorld;
  • 使用世界(Using a World)

世界类包含了创建和销毁物体和连接器的方法工厂。稍后会对这些工厂进行介绍。现在给大家介绍一下与b2World其它方面的交互。

  • 模拟(Simulation)

世界类用来驱动模拟。你指定了一个时间步长,速度以及位置迭代次数。比如说:

1
2
3
4
float32 timeStep = 1.0f/60.0f;
int32 velocityIterations = 10;
int32 positionIterations = 8;
myWorld->Step(timeStep, velocityIterations, positionIterations);

执行这次时间步长之后你可以测试一下物体和连接器相关的信息。更多的你可能将会看到物体的移动,这样你就可以不断的更新和重绘它们。你可以在游戏循环的任何位置来执行。比如说,如果你想在某一帧获取新创建物体的碰撞结果,那么你必须在执行此时间步长之前来创建物体。

就像在HelloWorld章节中所提到的,上面的时间步长你应该使用固定值。在低帧率的场景中通过使用一个大的时间步长可以改进性能。但是一般来说你应该使用一个不能大于1/30秒的时间步长。一个1/60秒的时间步长可以产生一个高质量的模拟。

迭代次数控制了约束求解器扫描世界中所有接触和连接器的次数。更多的迭代总是能够产生更高质量的模拟。但是不要为一个小的时间步长设置一个大的迭代数。60Hz搭配10次迭代要远好于30Hz搭配20次迭代。

下一步,你应该清空所有施加在物体上的力。可以使用b2World::ClearForces方法来完成。这可以让你在同一个力场完成多个分步。

1
myWorld->ClearForces();
  • 探索世界(Exploring the World)

世界包括物体,接触以及连接器。你可以获取世界中的物体,接触以及连接器链表并对它们进行遍历。举例来说,下面这段代码唤醒了世界中的所有物体:

1
2
3
4
for(b2Body* b = myWorld->GetBodyList(); b; b = b->GetNext())
{
    b->SetAwake(true);
}

不幸的是真实的程序可能会更复杂。比如说,下面的代码就中断了。

1
2
3
4
5
6
7
8
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()方法就会返回一个错误指针。解决办法是在获取下一个指针之前先对其进行拷贝操作。

1
2
3
4
5
6
7
8
9
10
11
b2Body* node = myWorld->GetBodyList();
while(node)
{
    b2Body* b = node;
    node = node->GetNext();
    GameActor* myActor = (GameActor*)b->GetUserData();
    if(myActor->IsDead())
    {
        myWorld->DestroyBody(b);
    }
}

这样就可以安全销毁当前的物体。即便如此你也可能会调用一个游戏方法产生销毁多个物体。这种情况你需要非常小心。这是对特定情况下的解决方案,但是为了方便起见,我会向你展示一个解决此问题的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
b2Body* node = myWorld->GetBodyList();
while(node)
{
    b2Body* b = node;
    node = node->GetNext();
    GameActor* myActor = (GameActor*)b->GetUserData();
    if(myActor->IsDead())
    {
        bool otherBodiesDstroyed = GameCrazyBodyDestroyer(b);
        if(otherBodiesDestroyed)
        {
            node = myWorld->GetBodyList();
        }
    }
}

这样做很明显,GameCrazyBodyDestroyer必须对于哪些物体销毁要非常的诚实。

  • AABB查询(AABB Queries)

有时候你需要在一个区域中决定所有的形状。b2World类提供了一个log(N)的快速算法来使用broad-phase数据结构。在世界坐标系内提供了一个AABB(axis-aligned bounding box),之后实现了b2QueryCallback回调。世界将会调用你的类,通过定制器遍历与查询AABB以确定是否有重叠的AABB。返回true继续查询,否则返回false。例如,接下来的代码找出与一个特定AABB有潜在交互的所有定制器并唤醒所有关联的物体。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class MyQueryCallback : public b2QueryCallback
{
    public:
        bool ReportFixture(b2Fixture* fixture)
        {
            b2Body*  body = fixture->GetBody();
            body->WakeUp();
            // Return true to continue the query.
            return true;
        }
};
...
MyQueryCallback callback;
b2AABB aabb;
aabb.lowerBound.Set(-1.0f, -1.0f);
aabb.upperBound.Set(1.0f, 1.0f);
myWorld->Query(&callback, aabb);

关于回调你不能假定回调的顺序。

  • 光线投射(Ray Casts)

你可以使用光线投射来做瞄准线检查(line-of-sight checks),火枪,等等。你可以通过实现一个回调类来实现光线投射的始点和终点。世界类会调用你的类访问射线中碰到的每一个定制器。回调中提供了定制器,交点,单位法向量(unit normal vector),以及沿着射线通过的的分数距离(fractional distance)。关于回调的次序你不能做任何假设。

你可以通过返回的分数来控制光线投射的延伸。返回分数(fraction)为零代表光线投射应该终止。返回分数(fraction)为一,光线好像没有发生碰撞一样一直延伸。如果从参数列表中返回分数,光线将被截断在当前位置和交点之间。所以你可以投射任何形状,甚至是所有形状,或者根据返回的适当分数投射最近的形状。

你可以返回-1分数值来以此过滤定制器。那么光线投射将会继续延伸就像定制器不存在一样。

这里是一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//This class captures the closest hit shape
class MyRayCastCallback : public b2RayCastback
{
    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;
};
MyRayCastCallback callback;
b2Vec2 point1(-1.0f, 0.0f);
b2Vec2 point2(3.0f, 1.0f);
myWorld->RayCast(&callback, point1, point2);

警告
由于误差的原因,在静态环境里,光线投射可能会潜在穿过形状之间的小的裂缝。如果这一点在你应用中不能接受,那么就请略微放大你的形状。

1
2
3
4
void SetLinearVelocity(const b2Vec2& v);
b2Vec2 GetLinearVelocity() const;
void SetAngularVelocity(float32 omega);
float32 GetAngularVelocity() const;
  • 力和冲量(Forces and Impulses)

你可以对一个物体作用力,扭矩和冲量。当你作用一个力或者一个冲量的时候,就在世界坐标下的提供了一个负载点。通常的结果会有一个关于质心的扭矩。

1
2
3
4
void ApplyForce(const b2Vec2& force, const b2Vec2& point);
void ApplyTorque(float32 torque);
void ApplyLinearImpulse(const b2Vec2& impulse, const b2Vec2& point);
void ApplyAngularImpulse(float32 impulse);

应用力,扭矩或者冲量唤醒物体。有时候这是不可取的。举例来说,你可能会作用一个恒定的力同时允许物体进入睡眠状态以此来提高性能。在这个场景中你可以使用下面代码。

1
2
3
4
if(myBody->IsAwake() == true)
{
    myBody->ApplyForce(myForce, myPoint);
}
  • 坐标变换(Coordinate Transformations)

物体类(body class)有一些辅助方法来帮助你对坐标和向量在局部和世界坐标系之间变换。如果你不清楚这些概念,请参阅“Essential Mathematics for Games and Interactive Applications“由Jim Van Verth和Lars Bishop合著。这些方法很高效(当内联使用时(when inlined))。

1
2
3
4
b2Vec2 GetWorldPoint(const b2Vec2& localPoint);
b2Vec2 GetWorldVector(const b2Vec2& localVector);
b2Vec2 GetLocalPoint(const b2Vec2& worldPoint);
b2Vec2 GetLocalVector(const b2Vec2& worldVector);
  • 链表(Lists)

你可以遍历一个物体的定制器。如果你想访问定制器的用户数据(user data)这个很有用。

1
2
3
4
5
for(b2Fixture* f = body->GetFixtureList(); f; f = f ->GetNext())
{
    MyFixtureData*  data = (MyFixtureData*)f->GetUserData();
    ...do something with data...
}

类似的你可以遍历物体的连接器链表。

物体也提供了一个接触相关的链表。你可以获取当前接触的链表。不过要小心,因为接触链表也许并不包括所有接触,因为可能并不包含前一个时间步长存在的接触。

Comments