Box2D v2.3.0 用户指南(第十章)

103 篇文章 0 订阅
53 篇文章 0 订阅



第十章 世界类(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)的列表,你可以通过这个列表来获取当前的接触数据。要小心的是,这个列表中不会包括只在前一个时间间隔内存在的那些接触。


  • 85
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值