Oh!Coder

Coding Life

Box2D C++ 教程-查询 World

| Comments

声明:本文翻译自Box2D C++ tutorial-World querying,仅供学习参考。

查询World

通常你可能想知道在给定的场景中都有哪些实体。例如有一个炸弹爆炸了,周围的所有实物都会受到不同程度的破坏,那么在RTS(译者注:Real-Time Strategy,即时战略)游戏当中,当玩家拖拉一个选框的时候,玩家可以合理的选中其中尚未完全破坏的单位。这种快速查询部分场景中尚未被完全损坏的单位的方法及其细节,就属于“查询World”的范畴。

Box2D提供了两个工具来完成这个功能-射线投射和AABB框测试。呃,射线投射是…别忘了上一篇刚刚介绍过哦!在上一篇中我们使用近似纯手工的方法实现的这个功能,当时通过遍历World中的定制器(fixture)来检测哪个物体距离射线原点最近。当场景中有大量物体时候,这种方法很低效。一个更好的办法是使用world自有的RayCast方法。此方法可以通过引擎来捕捉射线路径附近的定制器。

射线投射,高效的遍历方法

既然上一篇中我们已经对射线投射已经做过详细的介绍了,那么这里我们就不做额外的示范了,直接来看world中的RayCast方法如何使用。完整的方法声明:

1
void RayCast(b2RayCastCallback* callback, const b2Vec2& point1, const b2Vec2& point2);

方法中最后两个参数很简单,就是射线的起点和终点,通过这些参数我们就可以检测射线是否与单个定制器有交点。其中’callback’是个新参数,但是到目前为止我们应该已经熟悉了Box2D中回调方法通常是如何工作的了,所以这对于我们实现一个b2RayCastCallback的子类并没什么感到可奇怪的,然后我们实例化一个对象作为callback参数传入方法当中。b2RayCastCallback类只有一个可覆盖的方法,它就是:

1
2
//in b2RayCastCallback class
float32 ReportFixture(b2Fixture* fixture, const b2Vec2& point, const b2Vec2& normal, float32 fraction);

当射线进行投射计算的时候,每当与定制器有碰撞,callback方法就会被调用。对于每一次碰撞我们都能够知道当前与哪个定制器有交点,交点是多少,定制器“表面”的法线是多少以及沿着point1和point2射线方向上的交点段值是多少。如果你忘记了fraction参数的具体含义,可以回头温习下射线投射话题。

最后需要说的一点是处理callback方法的返回值问题。注意如果你的射线足够长,可能会和很多定制器发生碰撞,在一次RayCast的期间可能会多次调用回调方法。非常重要的一点是,射线并不会为你按照从近到远的顺序排列所检测到的定制器,它只会以上一次检测到的顺序依次排序-这对于处理大场景非常高效。引擎允许你自己处理射线所遇到的每一个定制器。这就是callback方法中返回值的来由。callback方法中你将会返回一个浮点型数值,在投射过程中你可以通过设置这个值来调整射线的长度。看到这里可能有一小点困惑,让我们放慢脚步…

  • 返回-1意味着忽略当前交点
  • 返回0~1来调整射线的长度,例如:
    • 返回0意味着根本没有射线
    • 返回1意味着射线的长度保持不变
    • 返回fraction值让射线保持至少能够与定制器有交点的长度

这里的fraction值指的是callback方法中传入的’fraction’参数(译者注:这里指的是ReportFixture方法当中传入的名为fraction的参数)。如果有人被我搞糊涂了,以后我可以为此做一张图,要是上面的描述不是很清楚的话就记住下面的通用规则:

  • 只找出最近的交点:
    -直接返回callback方法当中的fraction参数
    -使用最近一次的交点作为结果值
  • 沿着射线找出所有交点:
    -通过callback方法返回1
    -在列表中存储所有交点
  • 只是简单的找出射线的是否有交点:
    -如果callback被调用了,说明有交点(但不一定是最近的交点)
    -为了高效,把callback方法返回值设为0

区域查询(又名AABB查询)

在给定区域内,Box2D有另外一个方法来找出所覆盖的定制器,就是AABB查询。根据这一点我们可以定义一个矩形区域,引擎可以帮助我们找出矩形区域当中的所有定制器,并为其中的每一个定制器进行方法回调。通常情况下callback方法可以对定制器链表上的定制器做一些设置,为接下来的操作做一些准备,但是callback方法也允许你高效的实现一些其它操作。例如在给定区域有大量的定制器,但是呢你又想确定它们的总质量是否大于给定的数值,那么你可以在进行查询的时候累计它们的质量,当你能够确定结果的时候就停止查询。

这种方法称作AABB查询(QueryAABB),此名字的由来也是因为区域被指定为’axis-aligned bounding box(译者注:或许可以翻译成’同轴包围盒’?)’。这也就意味着区域是矩形的而且不能够以任意角度旋转。这也就意味着测试定制器是否在区域中,其实就是在测试定制器的AABB是否在区域中。你可以在testbed右面的面板上勾选渲染定制器AABB的选项。这个例子里显示了一些定制器的AABB(紫色)。

pic

此方法如下:

1
void QueryAABB(b2QueryCallback* callback, const b2AABB& aabb);

你喜欢用回调方法吗?我希望你喜欢,因为下边又有一个回调方法。像之前一样,你需要定义一个b2QueryCallback类的子类并且实现一个方法。类中的这个方法如下:

1
bool ReportFixture(b2Fixture* fixture);

这个不错,只有一个参数。既然AABB查询只是为了确定一个定制器是否在给定区域内,就没有必要像上面一样搞那么详细了。当你调用QueryAABB方法的时候,你的ReportFixture方法将会针对区域中的每个定制器进行回调,通过参数fixture进行传递。让我们用这个特性做一个例子。

我们将创建一个矩形区域,然后对区域中当前所覆盖的每一个定制器做标记。这里我们还是使用上一个话题中所使用的场景,从这里拷贝场景代码。

pic

首先获取屏幕上所显示的区域。我们可以覆盖Test类中MouseUp和MouseDown方法来获取鼠标坐标,接下来可以作为矩形的角的坐标,紧接着作为成员变量存储到FooTest类中。接下来在Step()方法中,使用Footest类中已存储的坐标画出一个矩形:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//FooTest class member variable
b2Vec2 mouseDownPos, mouseUpPos;

//FooTest class functions
void MouseDown(const b2Vec2& p) { mouseDownPos = mouseUpPos = p; Test::MouseDown(p); }
void MouseUp(const b2Vec2& p) { mouseUpPos = p; Test::MouseUp(p); }

//in Step() function, draw the rectangle
b2Vec2 lower( min(mouseDownPos.x,mouseUpPos.x), min(mouseDownPos.y,mouseUpPos.y) );
b2Vec2 upper( max(mouseDownPos.x,mouseUpPos.x), max(mouseDownPos.y,mouseUpPos.y) );

glColor3f(1,1,1);//white
glBegin(GL_LINE_LOOP);
glVertex2f(lower.x, lower.y);
glVertex2f(upper.x, lower.y);
glVertex2f(upper.x, upper.y);
glVertex2f(lower.x, upper.y);
glEnd();

不幸的是,此Box2D版本(v2.1.2)的testbed框架不允许我们覆盖MouseMove方法,所以我们不能直接看到当我拖拽鼠标的时候所画的矩形。

pic

使用区域查询过步骤中,现在我们可以真正得到AABB查询自身的信息。首先让我们创建b2QueryCallback类的子类并持有回调方法,然后用一个链表来保存查询区域中的定制器。你需要把链表设置成全局的或者是类可见的,但是区域查询的结果通常都是临时结果,更直观的方法也可以把结果存储在回调类中的链表里,然后每次用一个新实例来遍历这个链表。

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
26
27
//subclass b2QueryCallback
class MyQueryCallback : public b2QueryCallback {
public:
    std::vector<b2Body*> foundBodies;

    bool ReportFixture(b2Fixture* fixture) {
        foundBodies.push_back( fixture->GetBody() );
        return true;//keep going to find all fixtures in the query area
    }
};

//in Step() function
MyQueryCallback queryCallback;
b2AABB aabb;
aabb.lowerBound = lower;
aabb.upperBound = upper;
m_world->QueryAABB( &queryCallback, aabb );


//draw a point on each body in the query area
glPointSize(6);
glBegin(GL_POINTS);
for (int i = 0; i < queryCallback.foundBodies.size(); i++) {
    b2Vec2 pos = queryCallback.foundBodies[i]->GetPosition();
    glVertex2f( pos.x, pos.y );
}
glEnd();

pic

注意我们需要让callback方法一直返回true,以确保查询区域中包含的定制器全部存储到链表中。你可以看到在鼠标所画AABB区域当中包含的所有定制器上都会有白点。在上面的截图中,你可以看到例子的左下方,有一个定制器本身并没有在查询AABB范围内,但是也有白点的出现(译者注:原因很简单,因为左下角的圆形AABB框和查询AABB框有重叠,只不过圆形的AABB框没有显示出来而已:D )。

Comments