Chapter 11 杂项
11.1 用户数据
b2Fixture, b2Body 和 b2Joint 类都允许你通过一个 void 指针来附加用户数据。当你测试Box2D数据结构,并使其跟自己游戏引擎中的对象结合起来时,这样做是比较方便的。
举个典型的例子,角色上附有物体,并在物体中附加角色的指针,这就构成了一个循环引用。如果你有角色(actor),你就能得到物体。如果你有物体,你也能得到角色。
GameActor* actor = GameCreateActor();
b2BodyDef bodyDef;
bodyDef.userData = actor;
actor->body = box2Dworld->CreateBody(&bodyDef);
一些需要用户数据的例子:
• 使用碰撞结果给角色施加伤害效果。
• 当玩家进入一个包围盒(axis-aligned box)时,触发脚本事件。
• 当Box2D通知你关节将要被摧毁时,去访问某个游戏结构。
记住,用户数据是可选的,并且能放入任何东西。然而,你需要确保一致性。例如,如果你想在body中保存actor的指针,那你就应该在所有的 body中都保存actor指针。不要在一个body中保存actor指针,却在另一个body中保存foo指针。将一个actor指针强制转成foo指 针,可能会导致程序崩溃。
用户数据指针默认为NULL。
对于fixture来说,你可以定义一个用户数据结构来存储游戏特定的信息。例如材料类型、特效钩子、音效钩子,等等。
struct FixtureUserData
{
int materialIndex;
…
};
FixtureUserData myData = new FixtureUserData;
myData->materialIndex = 2;
b2FixtureDef fixtureDef;
fixtureDef.shape = &someShape;
fixtureDef.userData = myData;
b2Fixture* fixture = body->CreateFixture(&fixtureDef);
…
delete fixture->GetUserData();
fixture->SetUserData(NULL);
body->DestroyFixture(fixture);
11.2 隐式摧毁
Box2D没有使用引用计数。因此你摧毁了body后,它就确实不存在了。访问指向已摧毁body的指针,会导致未定义的行为。 也就是说,你的程序可能会崩溃。以debug方式编译出的程序,Box2D的内存管理器会将已被摧毁实体占用的内存,都填上FDFDFDFD。在某些时候, 这样做可以使你更容易的找到问题的所在,并进而修复问题。
如果你摧毁了Box2D实体,你要确保所有指向这实体的引用都被移除。如果只有实体的单个引用,处理起来就很简单了。但如果有多个引用,你需要考虑是否去实现一个句柄类(handle class),将原始指针封装起来。
当你使用Box2D时,会很频繁的创建并摧毁很多的物体(bodies)、形状(shapes)和关节(joints)。某种程度上,这些实体是由Box2D自动管理的。当你摧毁了一个body,所有跟它有关联的形状、关节和接触都会自动被摧毁。这称为隐式摧毁。
连接到这些关节或接触上的物体会被唤醒。这个过程是很简便的,但是你必须小心一个关键问题:
注意
当body被摧毁时,所有依附其上的形状和关节也会被自动摧毁。你应该将任何指向这些形状和关节的指针清零。否则如果你之后试图访问或者再次摧毁这些形状或关节,你的程序会死得很惨。
为了帮助你清空关节指针,Box2D提供了一个名叫b2DestructionListener的监听类。你可以实现这个类,并将world对象传给它。这样当关节被隐式摧毁时,world对象会通知你。
注意,关节或fixture被显式摧毁时,并没有通知。这种情况下,所有者是清楚的,你可以在合适的地方执行必要的清理工作。如果你喜欢,你也可以调用你自己的b2DestructionListener的实现来保持清理代码的集中。
大多数情况下,隐式摧毁还是很方便的,但也很容易使你的程序崩溃。你可能会将指向shape或关节的指针保存起来,当有关联的body摧毁时,这些指针就变得无效了。当关节是由那些与相关的物体的管理代码无关的代码片段创建时,事情会变得更糟糕。比如,testbed就创建了一个 b2MouseJoint用来交互式操作屏幕上的body。
Box2D提供了一种回调机制,用于在隐式摧毁发生时,通知你的应用程序。这给了你的程序一个机会,将无效指针清零。这个回调机制将在稍后详述。
你可以实现一个b2DestructionListener,这样当一个形状或关节隐式摧毁时,b2World 就能通知你。这将防止你的代码访问野指针。
class MyDestructionListener : public b2DestructionListener
{
void SayGoodbye(b2Joint* joint)
{
// remove all references to joint.
}
};
你可以将摧毁监听器(destruction listener)注册到world对象中。在world对象初始化时,你就应该这样做了。
myWorld->SetListener(myDestructionListener);
11.3 像素和坐标系统
重申一下Box2D使用MKS(米、千克和秒)单位制,角度使用弧度为单位。你可能对使用米感到困惑,因为你的游戏是以像素的形式表示的。在testbed中为了解决这个问题,我将整个游戏都使用米,并使用OpenGL的视图转换,将world调整到屏幕空间中。
float lowerX = -25.0f, upperX = 25.0f, lowerY = -5.0f, upperY = 25.0f;
gluOrtho2D(lowerX, upperX, lowerY, upperY);
如果你的游戏必须使用像素为单位,在向Box2D传送值的时候,你应该将你的长度单位由像素转换成米。反之,当你接收Box2D传来的值时,应该将之由米转换成像素。这会提升物理模拟的稳定性。
你必须设置一个合理的转换因子。我建议可以根据你的角色的尺寸来做出选择。假设你每米使用50个像素(因为你的角色有75个像素的高度),则你可以使用下面这些公式将像素转换成米:
xMeters = 0.02f * xPixels;
yMeters = 0.02f * yPixels;
相反的:
xPixels = 50.0f * xMeters;
yPixels = 50.0f * yMeters;
你应该在你的代码中使用MKS单位制,只在你渲染的时候,将之转换成像素。这会简化你的游戏逻辑,并减少错误的可能。因为渲染时的变换,能被限定在很小的代码中。
如果你使用转换因子,你应该在全局范围内调整它,确保没有错误发生。你也可以调整它来改善稳定度。