Bullet 是一个开源的 3D 碰撞检测和物理引擎库,只需要几行代码就可以添加碰撞检测。本教程继续上一篇将指导您使用 LibGDX 3D物理引擎 Bullet 的API。
在上一篇中,我们已经了解了什么是 Bullet 包装器,以及我们如何使用它的碰撞检测。我们将使用它的代码作为本教程这一部分的基础。
如果查看 Bullet 包装器的源代码,您将看到它由五个部分组成,每个部分都有自己的 java 包。这些是:
linearmath(线性数学)包包含一些与物理学没有直接关系的通用类和方法。例如,btvector3类是LibGDX vector3类到bullet的桥接。collision(碰撞)包包含了我们之前看到的所有与碰撞检测相关的东西,比如碰撞形状、碰撞世界以及近相和广相。dynamics包包含了所有与刚体动力学相关的内容,我们将在本教程中介绍。softbody包含每一个相关的软体和布料模拟(相对于刚体)。最后,extras包包含一些有用的帮助类/工具。
在应用动力学之前,我们需要设置 Bullet 执行某些计算所需的一些属性。例如,当你推(施加力)一个物体时,物体的重量(质量)对所发生的事情很重要。如果物体非常重,它可能根本不会移动。但是当它很轻的时候,它可以移动很远的距离。其他性质,比如物体和物体表面之间的摩擦力也是相关的。因为 btCollisionObject 不包含这样的属性,所以我们需要使用一个名为 btRigidBody 的子类。顾名思义,这个类包含刚体的所有属性。
修改上一篇的代码如下:
public class MainGame implements ApplicationListener {
...
static class GameObject extends ModelInstance implements Disposable {
public final btRigidBody body;
public boolean moving;
public GameObject (Model model, String node, btRigidBody.btRigidBodyConstructionInfo constructionInfo) {
super(model, node);
body = new btRigidBody(constructionInfo);
}
@Override
public void dispose () {
body.dispose();
}
static class Constructor implements Disposable {
public final Model model;
public final String node;
public final btCollisionShape shape;
public final btRigidBody.btRigidBodyConstructionInfo constructionInfo;
private static Vector3 localInertia = new Vector3();
public Constructor (Model model, String node, btCollisionShape shape, float mass) {
this.model = model;
this.node = node;
this.shape = shape;
if (mass > 0f)
shape.calculateLocalInertia(mass, localInertia);
else
localInertia.set(0, 0, 0);
this.constructionInfo = new btRigidBody.btRigidBodyConstructionInfo(mass, null, shape, localInertia);
}
public GameObject construct () {
return new GameObject(model, node, constructionInfo);
}
@Override
public void dispose () {
shape.dispose();
constructionInfo.dispose();
}
}
}
...
@Override
public void create () {
...
constructors = new ArrayMap<String, GameObject.Constructor>(String.class, GameObject.Constructor.class);
constructors.put("ground", new GameObject.Constructor(model, "ground", new btBoxShape(new Vector3(2.5f, 0.5f, 2.5f)), 0f));
constructors.put("sphere", new GameObject.Constructor(model, "sphere", new btSphereShape(0.5f), 1f));
constructors.put("box", new GameObject.Constructor(model, "box", new btBoxShape(new Vector3(0.5f, 0.5f, 0.5f)), 1f));
constructors.put("cone", new GameObject.Constructor(model, "cone", new btConeShape(0.5f, 2f), 1f));
constructors.put("capsule", new GameObject.Constructor(model, "capsule", new btCapsuleShape(.5f, 1f), 1f));
constructors.put("cylinder", new GameObject.Constructor(model, "cylinder", new btCylinderShape(new Vector3(.5f, 1f, .5f)), 1f));
...
}
}
这里我们用 btRigidBody 类替换了 GameObject 类的 btCollisionObject,它扩展了 btCollisionObject。要构造 body,我们现在使用 btRigidBodyConstructionInfo 类。这个类包含了我们之前使用过的 btCollisionShape,但也包含了质量、摩擦、阻尼等其他属性。构造函数类现在也包含了这个 constructionInfo,这允许一种非常方便的方式来构造同一个对象的多个实例。
在创建 btRigidBodyConstructionInfo 时,我们需要指定质量(对象的重量) ,我们对第二个参数使用 null (我们很快就会知道为什么) ,接下来我们提供 btCollisionShape 作为第三个参数,最后我们需要指定 localinertial。Localinertial 是一个静态 Vector3,因此它将被每个构造函数重用。如果质量等于或小于零,我们只要把局部惯性也设为零。否则,我们需要计算局部交互性。幸运的是,碰撞形状有一个很好的辅助方法来计算它。
请注意,Constructor 类仍然保存 btCollisionShape,尽管 btRigidBodyConstructionInfo 也包含此信息。这是因为我们仍然拥有的形状和需要处理时,不再需要它。当然对于 constructionInfo 成员也是一样的,所以我在 dispose 方法中添加了一行来释放它。
我们用于测试的构造函数大部分仍然是相同的,但是我们现在必须在创建构造函数时包含质量。现在我们只需要对每个物体使用质量为 1f,除了地面,我们使用质量为 0f。
零质量在物理上是不可能的。它用来表示地面不应该受到任何作用于它的力的影响。它应该始终保持在同一位置(和旋转) ,不管任何力量或碰撞可能适用于它。这就是所谓的“静态”对象。其他质量大于零的物体称为“动态”物体。
大多数物理属性需要使用单位来指定。例如,质量通常以千克为单位。对象的大小以米为单位,时间以秒为单位。这些被称为 SI 单位。建议尽可能使用它们。但有时使用它们是不现实的。例如,当你的对象非常大或非常小的时候。
当值大约为1(1f)时,Bullet 表现最佳。有很多因素(比如浮点精度) ,但实际上最好将 btRigidBody 的属性值保持在1左右。所以,如果你正在制作一个空间游戏,游戏对象可能比一米大很多,你可能想要决定使用 dekameter (10) ,hectometer (100)或 km (1000)代替。或者,如果你正在制作一个游戏,其中的物体可能比1米小得多,你可能想要决定使用分米(0.1)、厘米(0.01)或毫米(0.001)。同样地,如果你的物体可能比一公斤重或轻得多,你可能也想测量一下它的质量。
扩展你的单位通常是没有问题的,只要你保持一致。因此,如果你决定使用英寸而不是米,那么你也应该使用英寸每秒表示速度,英寸每秒表示加速度(如重力) ,千克每秒表示力量。
请记住,在大多数情况下,(可视化)模型的大小与物理体的大小相同,模型实例的变换矩阵不能包含缩放组件。因此,您的模型的大小应该与所需的单位匹配,并且在从您的建模应用程序中导出模型之前,应用的任何缩放都应该“baked”。
为了实际应用动态,我们需要添加一个动态世界(btDynamicsWorld)。这将取代(并扩展)我们以前使用过的 btCollisionWorld:
public class MainGame implements ApplicationListener {
...
private btDynamicsWorld dynamicsWorld;
private btConstraintSolver constraintSolver;
@Override
public void create () {
...
collisionConfig = new btDefaultCollisionConfiguration();
dispatcher = new btCollisionDispatcher(collisionConfig);
broadphase = new btDbvtBroadphase();
constraintSolver = new btSequentialImpulseConstraintSolver();
dynamicsWorld = new btDiscreteDynamicsWorld(dispatcher, broadphase, constraintSolver, collisionConfig);
dynamicsWorld.setGravity(new Vector3(0, -10f, 0));
contactListener = new MyContactListener();
instances = new Array<GameObject>();
GameObject object = constructors.get("ground").construct();
instances.add(object);
dynamicsWorld.addRigidBody(object.body, GROUND_FLAG, ALL_FLAG);
}
@Override
public void render() {
final float delta = Math.min(1f / 30f, Gdx.graphics.getDeltaTime());
dynamicsWorld.stepSimulation(delta, 5, 1f/60f);
for (GameObject obj : instances)
obj.body.getWorldTransform(obj.transform);
...
}
public void spawn () {
GameObject obj = constructors.values[1 + MathUtils.random(constructors.size - 2)].construct();
obj.transform.setFromEulerAngles(MathUtils.random(360f), MathUtils.random(360f), MathUtils.random(360f));
obj.transform.trn(MathUtils.random(-2.5f, 2.5f), 9f, MathUtils.random(-2.5f, 2.5f));
obj.body.setWorldTransform(obj.transform);
obj.body.setUserValue(instances.size);
obj.body.setCollisionFlags(obj.body.getCollisionFlags() | btCollisionObject.CollisionFlags.CF_CUSTOM_MATERIAL_CALLBACK);
instances.add(obj);
dynamicsWorld.addRigidBody(obj.body, OBJECT_FLAG, GROUND_FLAG);
}
...
@Override
public void dispose () {
...
dynamicsWorld.dispose();
constraintSolver.dispose();
...
}
}
为了构建动态世界,我们需要一个 btConstraintSolver, 这个类用于解决约束(简单地说: 可以使用约束将对象相互附加)。对于动态世界,我们使用 btDiscreteDynamicsWorld 实现。
在我们创造了动态世界之后,我们也设定了世界的引力。这将导致所有的动态物体(但不是静态地面)有重力适用于他们。为了我们的测试,我们将使用沿 y 轴每秒负10米的重力。这接近于地球引力。
我们现在不使用 addCollisionObject 方法,而是使用 addRigidBody 方法将对象添加到这个世界中。注意,addCollisionObject 方法仍然可用,但 addRigidBody 方法确保例如重力正确地应用于每个对象。
由于物体现在会因为重力而下落,我们不必手动移动物体。相反,我们需要指示世界运用重力,更新 body 的变形。
@Override
public void render () {
final float delta = Math.min(1f / 30f, Gdx.graphics.getDeltaTime());
dynamicsWorld.stepSimulation(delta, 5, 1f/60f);
for (GameObject obj : instances)
obj.body.getWorldTransform(obj.transform);
...
}
使用了世界的 stepSimulation 方法代替 performDiscreteCollisionDetection 方法。这将在内部调用 performDiscreteCollisionDetection 方法(包括碰撞检测回调)。
离散动力学世界使用固定的时间步长。这基本上意味着它将始终使用相同的 delta 值来执行计算。这个固定的 delta 值作为 stepSimulation 的第三个参数提供。如果实际的 delta 值(第一个参数)大于期望的固定 delta 值,那么计算将进行多次。这样做的最大次数(子步骤的最大次数)由第二个参数指定。注意,我们仍然将 delta 上限设置为1f/30f,因此子步骤的实际数量永远不会超过2。如果您想了解更多关于它的信息,有相当多的关于修复时间步骤的资源。然而,在实践中,Bullet 为我们解决了这个问题,只要你理解其中的论点,它就应该没问题。
因为 Bullet 现在转换(翻译和旋转)我们的对象,所以我们需要向 Bullet 请求新的转换并将其设置为 ModelInstance#transform。这是通过调用 obj.body.getWorldTransform (obj.transform) ; 完成的。
运行代码,你会发现它实际上已经做了我们差不多想要的了。这些物体在静止的地面上受到重力的作用而坠落下来。由于我们使用了碰撞过滤器,如本教程前面部分所示,对象之间不会相互响应(它们彼此相互落入)。
由于世界现在完全控制移动对象,就不再需要游戏对象类的moving属性了。因此,我们在 MyContactListener 中删除moing属性,为了验证MyContactListener是否仍然被触发,我们可以改变物体触地时的颜色。
public class BulletTest implements ApplicationListener {
...
class MyContactListener extends ContactListener {
@Override
public boolean onContactAdded (int userValue0, int partId0, int index0, int userValue1, int partId1, int index1) {
if (userValue0 != 0)
((ColorAttribute)instances.get(userValue0).materials.get(0).get(ColorAttribute.Diffuse)).color.set(Color.WHITE);
if (userValue1 != 0)
((ColorAttribute)instances.get(userValue1).materials.get(0).get(ColorAttribute.Diffuse)).color.set(Color.WHITE);
return true;
}
}
...
}
看看我们刚刚添加的代码:
@Override
public void render () {
...
for (GameObject obj : instances)
obj.body.getWorldTransform(obj.transform);
...
}
基本上,这要求项目符号的当前位置和每个对象的旋转。正如你所想象的,如果你有一个很大的世界,这可能是相当多的对象,轮询他们的位置和旋转在每个渲染调用。然而,在实践中,通常只有少数对象实际上被移动和/或旋转(如果有的话)。幸运的是 Bullet 提供了一种机制来告诉我们什么时候一个对象被转换,这样我们就不需要遍历所有的对象了。为此,它使用了一个名为 btMotionState 的小回调类。
btMotionState 类有两个方法,您必须重写它们。无论何时,只要 Bullet 已经转换了动态对象,就会调用 setWorldTransform。每当需要知道对象的当前转换时,例如将对象添加到世界中时,Bullet 都会调用 getWorldTransform。
static class MyMotionState extends btMotionState {
Matrix4 transform;
@Override
public void getWorldTransform (Matrix4 worldTrans) {
worldTrans.set(transform);
}
@Override
public void setWorldTransform (Matrix4 worldTrans) {
transform.set(worldTrans);
}
}
这里我们实现了一个非常基本的 MyMotionState,只是更新了 matrix4实例。当然,也可以执行其他操作。例如,在我们的测试中,如果一个物体从地面上掉下来(该位置的 y 值低于零或某个阈值) ,我们可以将该物体从世界上移除。
现在我们需要通知 Bullet 使用这个 MotionState。
static class GameObject extends ModelInstance implements Disposable {
public final btRigidBody body;
public final MyMotionState motionState;
public GameObject (Model model, String node, btRigidBody.btRigidBodyConstructionInfo constructionInfo) {
super(model, node);
motionState = new MyMotionState();
motionState.transform = transform;
body = new btRigidBody(constructionInfo);
body.setMotionState(motionState);
}
@Override
public void dispose () {
body.dispose();
motionState.dispose();
}
...
}
在这里,我们构造 MotionState 并将其转换为 ModelInstance 的转换成员。注意,这是通过引用完成的,因此 MotionState 直接更新其transform成员。对 setMotionState 的调用将通知 Bullet 使用此 MotionState。这也会导致 Bullet 调用运动状态上的 getWorldTransform 以获得对象的当前转换。
我们还需要稍微修改一下 spawn 方法。
public void spawn () {
GameObject obj = constructors.values[1 + MathUtils.random(constructors.size - 2)].construct();
obj.transform.setFromEulerAngles(MathUtils.random(360f), MathUtils.random(360f), MathUtils.random(360f));
obj.transform.trn(MathUtils.random(-2.5f, 2.5f), 9f, MathUtils.random(-2.5f, 2.5f));
obj.body.proceedToTransform(obj.transform);
obj.body.setUserValue(instances.size);
obj.body.setCollisionFlags(obj.body.getCollisionFlags() | btCollisionObject.CollisionFlags.CF_CUSTOM_MATERIAL_CALLBACK);
instances.add(obj);
dynamicsWorld.addRigidBody(obj.body, OBJECT_FLAG, GROUND_FLAG);
}
这里唯一的变化是,我们删除了对主体上的 setWorldTransform 的调用,现在改为调用 proceedToTransform 方法。这指示 Bullet 不仅更新对象的世界/变换矩阵,而且更新所有其他相关成员。
最后,我们不再需要轮询 render 方法中的每个对象的转换,所以让我们移除这段代码。
@Override
public void render () {
final float delta = Math.min(1f / 30f, Gdx.graphics.getDeltaTime());
dynamicsWorld.stepSimulation(delta, 5, 1f/60f);
if ((spawnTimer -= delta) < 0) {
spawn();
spawnTimer = 1.5f;
}
...
}
MotionState 是非常强大的,并且非常容易处理。然而,使用MotionState的另一个好处可能看起来并不那么明显。为了理解这一点,请考虑调用 dynamicsWorld.stepSimulation(delta, 5, 1f/60f); 时发生的情况。Bullet 将使用指定的 1f/60f delta 时间执行尽可能多的计算(但不超过指定的最大值) ,直到达到指定的实际运行 delta 时间。显然,delta 值总是精确地等于1f/60f 的倍数是不太可能的。实际上,在某些情况下,delta 的值可能小于指定的1f/60f,导致 Bullet 根本不执行任何计算。
因此,使用 obj.body.getWorldTransform ()方法得到的转换不是当前时间的转换,而是最后计算的固定时间步骤的转换。为了补偿这一点,Bullet 通过插值计算出的变换来近似当前时间的变换。这个插值转换是在 MotionState 的 setWorldTransform 方法中提供给您的。
因此,使用一个 MotionState 将给你一个平滑的视觉过渡到时间步骤。注意可视化这个词,实际的碰撞检测和动态计算(包括回调)只在固定的时间步骤中完成。
Bullet 为我们提供了插值变换矩阵,虽然这看起来微不足道,但它可能会让你对 Bullet 的一些内部构造产生疑问。例如,让我们看看它是如何计算这个插值变换的。考虑一个球掉下来,就像我们在千前面的示例中所做的那样,但是没有任何其他对象。假设球从位置 x = 0,y = 0,z = 0开始,但为了简单起见,我们在这个例子中只考虑 y 坐标。在模拟开始的时候,球是没有速度的(它“落下”的米每秒为零)。我们只对球施加重力,即向下的加速度为每秒10米每秒:
position(0) = 0f;
velocity(0) = 0f;
acceleration = -10f;
有了这些信息,我们可以计算出球在任何给定时间的位置。一秒钟后,速度增加了10秒米每秒,球下降了5米,以此类推:
position(t) = 0.5 * acceleration * t * t;
position(0) = 0;
position(1) = -5;
position(2) = -20;
position(3) = -45;
position(4) = -80;
虽然在这个例子中你不需要理解这个方程式,但是如果你对它不熟悉,我还是建议你去学习相关的资料。
现在假设已经过去了3.5秒,我们想知道新的位置。最明显的方法是使用以上公式计算新位置: position(3.5) = 0.5 * -10 * 3.5 * 3.5 = -61.25; 这是最准确的方法。在大多数情况下都不可行。实际上,这与使用可变时间步长一样,有许多缺点。例如,它会漏掉在 time=0 和 time=3.5 之间发生的所有碰撞。而且由于每次的增量时间不同,结果不能再现(你不能重放物理模拟)。注意,您可以通过为 maxSubsteps 提供值0来指示 Bullet 采取这种方法,这是 stepSimulation 方法的第二个参数,但显然不建议这样做。
如前所述,我们使用固定的时间步长。假设我们有一个固定的时间步长为1秒。因此,我们在 time = 1 时计算位置(执行碰撞检测并响应它)。接下来,我们将在 time = 2 时计算位置(再次执行碰撞检测并响应它)。最后,我们将在 time = 3 时计算位置(执行碰撞检测并响应它)。然后我们将设法接近时间 = 3.5 的位置。让我们看看我们有什么选择:
position(3.5) = position(3) + 0.5 * (position(4) - position(3)) = -45 + 0.5 * -35 = -62.5
很接近了。但它有一个很大的缺点: 我们无法展望未来。为了计算 time = 4 时的位置,我们必须执行碰撞检测并在它真正发生之前对它做出响应。这将导致在还没有 contact 时调用 ContactListener。这种方法也存在一些实际问题,例如两个变换之间的插值会导致奇怪的结果
velocity(3) = 3 * -10 = -30;
position(3.5) = position(3) + (3.5 - 3) * velocity(3) = -45 + 0.5 * -30 = -60
这也很接近了。事实上,在相当长的一段时间里,它曾经是 Bullet 的默认选项。然而,这种方法的主要问题是,近似变换不一定要在 time = 4 时与实际位置保持一致。例如,如果碰撞在前面,或者由于其他原因,速度发生了变化。
velocity(3) = 3 * -10 = -30;
position(2.5) = position(3) - (3 - 2.5) * velocity(3) = -45 - 0.5 * -30 = -30
这种方法的关键因素是一致性。视觉表现总是正好落后一步。尽管这在大多数情况下并不明显,但是您可以在游戏逻辑中对此进行补偿。
Bullet 允许您使用 setLatencyMotionStateInterpolation 方法在方法2和3之间进行选择。例如选择方法2:
((btDiscreteDynamicsWorld)dynamicsWorld).setLatencyMotionStateInterpolation(false);
在这些方法中,我简化了可读性的计算。实际上,Bullet 使用了更加复杂和精确的计算来估计所需的转换。此计算的输入存储在冲突对象的专用成员中,这些成员称为插值世界变换、插值线性速度和插值角速度。您可以使用各自的方法获取和设置每个方法,例如: object.setinterpreationworldtransform (transform) ; 。还记得我们如何更改 spawn ()方法以使用 setWorldTransform 方法而不是 setWorldTransform 吗?Processedtotransform 方法也将更新插值值。
在本教程的前面部分,我们使用了碰撞过滤,使物体只与地面碰撞。让我们移除这个过滤器,让对象也相互碰撞。
public void create () {
...
dynamicsWorld.addRigidBody(object.body);
}
public void spawn () {
...
dynamicsWorld.addRigidBody(obj.body);
}
这个更改只是从 addRigidBody 调用中删除了GOUND_FLAG, ALL_FLAG 参数和 OBJECT_FLAG标志。如果你运行这个程序,你会发现这些对象现在也相互碰撞了:
但是现在,当物体与另一个物体碰撞时,它们也会变成白色,而我们只想在它们与地面碰撞时改变颜色。我们可以通过稍微修改 ContactListener 来解决这个问题:
class MyContactListener extends ContactListener {
@Override
public boolean onContactAdded (int userValue0, int partId0, int index0, int userValue1, int partId1, int index1) {
if (userValue1 == 0)
((ColorAttribute)instances.get(userValue0).materials.get(0).get(ColorAttribute.Diffuse)).color.set(Color.WHITE);
if (userValue0 == 0)
((ColorAttribute)instances.get(userValue1).materials.get(0).get(ColorAttribute.Diffuse)).color.set(Color.WHITE);
return true;
}
}
在这里,我们只在第二个对象是地面的情况下才改变第一个对象的颜色,同样,只有在第一个对象是地面的情况下才改变第二个对象的颜色。请记住,我们在这里使用 userValue,我们已经将它设置为实例数组中对象的索引。因为第一个对象是地面,所以它的索引为0。
虽然这对我们的测试有用,但它并不是最佳的。Bullet 不仅为我们检测碰撞,而且现在它还处理物体如何对这些碰撞做出反应。在大多数情况下,您希望 Bullet 在没有通知您的情况下做这件事。只有对于少量的碰撞,您实际上需要得到通知。显然,您可以检查回调中的冲突,就像我们现在在这个修改过的 ContactListener 中所做的一样。但是在这种情况下,对于每次冲突,包装器仍然必须在本地 Bullet 代码和 java 代码之间架起桥梁。幸运的是,包装器允许您指定希望它桥接到 ContactListener 的冲突。这被称为联系人回调过滤(有时简称为联系人过滤或回调过滤) ,特定于 Bullet 包装器。
联系人回调过滤的工作方式与冲突过滤非常相似(但与冲突过滤无关)。您需要为每个碰撞对象定义一个按位标志,并为希望调用回调的对象定义一个按位筛选器(掩码)。
public void create () {
...
dynamicsWorld.addRigidBody(object.body);
object.body.setContactCallbackFlag(GROUND_FLAG);
object.body.setContactCallbackFilter(0);
}
public void spawn () {
...
dynamicsWorld.addRigidBody(obj.body);
obj.body.setContactCallbackFlag(OBJECT_FLAG);
obj.body.setContactCallbackFilter(GROUND_FLAG);
}
这里我们告诉包装器,地面有位标志 GROUND_FLAG (这是我们在本教程前面部分中看到的第九位) ,当地面与另一个对象碰撞时,我们不需要被告知(地面的过滤器为零)。接下来,我们告诉 spawn 方法中的包装器,每个对象都有位标志 OBJECT_FLAG,并且当它与地面碰撞时,我们希望得到通知(对象的过滤器是 GROUND_FLAG)。如果需要,您可以组合多个标志来创建一个过滤器,例如: obj.body.setContactCallbackFilter(GROUND_FLAG | WALL_FLAG;
请注意,这与冲突过滤不同。对于冲突过滤,两个对象的过滤器需要匹配另一个对象的标志才能发生冲突,而对于联系人回调过滤器,只有一个过滤器需要匹配另一个对象的标志才能调用回调。
现在我们需要通知包装器实际使用联系人回调过滤。我们可以通过为回调方法使用另一个签名来实现这一点。正如我们在前面的部分中看到的,ContactListener 类有几个方法,我们可以重写这些方法,这取决于重写包装器将尽可能优化的方法的签名。
@Override
public boolean onContactAdded (int userValue0, int partId0, int index0, boolean match0,
int userValue1, int partId1, int index1, boolean match1) {
if (match0)
((ColorAttribute)instances.get(userValue0).materials.get(0).get(ColorAttribute.Diffuse)).color.set(Color.WHITE);
if (match1)
((ColorAttribute)instances.get(userValue1).materials.get(0).get(ColorAttribute.Diffuse)).color.set(Color.WHITE);
return true;
}
这里我们改变了方法签名,使其包含两个布尔参数: match0 和 match1。包装器将“看到”这个并因此应用联系人回调过滤。请注意,默认情况下,联系人回调过滤器将被设置为零,因此在不设置联系人回调标志和过滤器值的情况下重写此方法,将导致永远不触发该回调。还请注意,您可以选择是否使用每个回调方法的联系人回调筛选。例如,对于 onContactAdded 回调,下面的方法将使用联系人回调过滤,但是对于 onContactProcessed 回调不使用:
class MyContactListener extends ContactListener {
@Override
public boolean onContactAdded (int userValue0, int partId0, int index0, boolean match0,
int userValue1, int partId1, int index1, boolean match1) {
...
}
@Override
public void onContactProcessed(int userValue0, int userValue1) {
...
}
}
match0 和 match1值用于指示匹配哪个对象的过滤器。所以在我们的例子中,匹配变量将只针对与地面碰撞的物体设置,而不针对地面本身。
让我们通过上下移动地面来使我们的测试更有趣一点。由于地面是一个静止的物体,它不会受到物理学的影响,但是物理学会受到地面的影响。显然,移动的地面不再是静止的了。这样一个能够运动但对碰撞没有反应的物体称为运动体。在实践中,运动主体非常像一个静态对象,除了你可以通过代码改变它的位置和旋转。在我们开始移动地面之前,我们必须告知 Bullet,地面现在是一个运动体。
public void create () {
...
instances = new Array<GameObject>();
GameObject object = constructors.get("ground").construct();
object.body.setCollisionFlags(object.body.getCollisionFlags()
| btCollisionObject.CollisionFlags.CF_KINEMATIC_OBJECT);
instances.add(object);
dynamicsWorld.addRigidBody(object.body);
object.body.setContactCallbackFlag(GROUND_FLAG);
object.body.setContactCallbackFilter(0);
}
这里唯一的变化是对 setCollisionFlags 方法的调用,在这里我们将 CF_KINEMATIC_OBJECT 标志添加到地面主体。我们在之前的 spawn 方法中看到过这个方法,我们用它来添加 CF_CUSTOM_MATERIAL_CALLBACK 标志来通知 Bullet 我们想要接收 onContactAdded 回调。非常相似的是,CF_KINEMATIC_OBJECT 通知 Bullet,地面是一个运动学的 body,我们可能想要改变它的变换。
那么让我们移动地面:
float angle, speed = 90f;
@Override
public void render () {
final float delta = Math.min(1f / 30f, Gdx.graphics.getDeltaTime());
angle = (angle + delta * speed) % 360f;
instances.get(0).transform.setTranslation(0, MathUtils.sinDeg(angle) * 2.5f, 0f);
instances.get(0).body.setWorldTransform(instances.get(0).transform);
dynamicsWorld.stepSimulation(delta, 5, 1f/60f);
...
}
这里我们添加了一个角度和速度变量。角度变量将保持当前的角度,我们将基于地面的位置。速度变量以地面移动的每秒度数来表示速度。在 render 渲染方法中,我们相应地更新角度变量。接下来我们将地面位置的 y 坐标设置为这个角度的正弦值。这将使地面平滑地上下移动。最后,我们相应地设置了地面物理体的世界变换,导致 Bullet 使用这个新的位置来执行下一个模拟。
如果你运行它,你会发现它实际上已经做了我们想要的。地面平滑地上下移动,动态物体也相应地作出反应。但是过一段时间,你就会发现这个模拟有缺陷。物体在空中漂浮或在地面上反弹。
为了理解为什么会发生这种情况,请想象一个非常大的静态地面,上千个动态物体仍然躺在那里。很明显,每个动态物体和地面之间都会发生碰撞,因此,当执行广义相位碰撞和近相位碰撞算法时,包括接触回调在内的所有碰撞侦测都必须执行。所有的物体都必须在每一帧中完成,即使没有动力学要执行。
正如你可能理解的,这并不是很有效。这就是为什么 Bullet 可以让你指定哪些对象应该检查碰撞。因此,Bullet 将只检查指定对象与所有对象的碰撞,而不是检查所有对象与所有对象的碰撞。应该检查碰撞的对象称为活动对象。同样,不应该被检查的对象被称为休眠(或停用)对象。
虽然这本身就是一个纯粹的碰撞检测功能,动态层使它更加强大,自动激活和停用对象的需要。它通过监测物体的速度来做到这一点。当一个物体被加到这个世界上,或者当它在运动时,它就被激活了。一旦它的速度低于某个阈值(可以使用 setSleepingThresholds 方法设置该阈值)一段时间(可以使用 setDeactivationTime 方法设置该阈值) ,它就会被停用。如果所有相邻的对象也不被认为是活动的,那么它将被停用。这是通过激活状态完成的:
所以,在我们的代码中:
因为运动主体的位置和旋转是由代码设定的,而且它没有应用速度,所以它不会被 Bullet 自动激活。所以基本上我们必须设定,运动身体的状态,我们的自我。
public void render () {
...
instances.get(0).body.setWorldTransform(instances.get(0).transform);
instances.get(0).body.setActivationState(Collision.ACTIVE_TAG);
...
}
对于我们的测试来说,这就足够了,但是这并不是激活 body 的首选方法。这是因为计时器用于检测身体的速度低于阈值的时间。当通过代码激活 body 时,我们还需要重置计时器。幸运的是 Bullet 对于这个问题有一个很好的帮助方法叫做 activate () :
public void render () {
...
instances.get(0).body.setWorldTransform(instances.get(0).transform);
instances.get(0).body.activate();
...
}
所以,万一我们移动了一个物体,我们需要激活那个物体,以确保 Bullet 检查它是否有碰撞。过一会 Bullet 将自动再次停用 body。但是在我们的测试中,我们不断地移动地面,所以没有必要让 Bullet 自动使 body 失去活性。Bullet 还有两种额外的激活状态:
因此,通过设置激活状态为 DISABLE_DEACTIVATION 后创建地面,我们不必手动激活它在渲染方法了。
public void create () {
...
object.body.setActivationState(Collision.DISABLE_DEACTIVATION);
}
...
public void render () {
...
instances.get(0).body.setWorldTransform(instances.get(0).transform);
dynamicsWorld.stepSimulation(delta, 5, 1f/60f);
}
之前我们已经看到如何使用运动状态来同步 Bullet 物理体和我们的视觉游戏对象的世界变换矩阵。Bullet 自动调用主动运动体的运动状态的 getWorldTransform 以获得最新的转换。所以我们不必在移动它之后手动更新它:
public void render () {
final float delta = Math.min(1f / 30f, Gdx.graphics.getDeltaTime());
angle = (angle + delta * speed) % 360f;
instances.get(0).transform.setTranslation(0, MathUtils.sinDeg(angle) * 2.5f, 0f);
dynamicsWorld.stepSimulation(delta, 5, 1f/60f);
...
}
注意: 只要运动体是活动的,每次都会调用其运动状态的 getWorldTransform 方法。只有在你真正移动或旋转它的时候,你才应该让它保持活动状态。