许多 3D 游戏需要 3D 对象之间的某种碰撞检测。有时候可以通过一些基本的数学、边框和边框球来做到这一点。但当形状变得更复杂时,数学和代码也会变得更复杂。幸运的是,LibGDX 给我们封装了 Bullet 物理引擎。Bullet 是一个开源的 3D 碰撞检测和物理引擎库,只需要几行代码就可以添加碰撞检测。本教程将指导您使用 LibGDX 3D物理引擎 Bullet 的API。
由于 Bullet 库是用 C++ 编写的, 因此其性能非常好。它被许多商业游戏和电影所使用。但这也给我们带来了一个问题。使用 LibGDX,你可以用 Java 编写代码,但是你不能直接从 Java 中使用 C++ 库。事实上,这两种语言的设计是如此不同,以至于在许多情况下,一对一的翻译而不损失性能是不可能的。这就是“包装器”的用武之地。包装器是 Bullet 库和 Java 应用程序之间的一个层(如果您愿意,可以称为“桥”) ,同时保持性能。正因为如此,包装器向 Bullet API 添加了一些更改。
这一篇教程中,我们不用之前教程的代码了,将从零开始编写代码。
那么,让我们开始编码吧!您可以继续创建一个新的 LibGDX 项目,我假设您对此已经很熟悉,并且不会一一介绍。如果使用此设置,还可以添加 gdx-bullet 扩展。如果没有,您将不得不以后手动添加它。当你所有的设置,创建一个类 继承 ApplicationListener:
import com.badlogic.gdx.ApplicationListener;
public class MainGame implements ApplicationListener{
@Override
public void create() {
}
@Override
public void resize(int width, int height) {
}
@Override
public void render() {
}
@Override
public void pause() {
}
@Override
public void resume() {
}
@Override
public void dispose() {
}
}
添加3D场景中基础的物体,如相机(Camera),相机控制器(CameraInputController),模型批处理(ModelBatch) 以及环境(Environment )等。
import com.badlogic.gdx.ApplicationListener;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.PerspectiveCamera;
import com.badlogic.gdx.graphics.g3d.Environment;
import com.badlogic.gdx.graphics.g3d.ModelBatch;
import com.badlogic.gdx.graphics.g3d.ModelInstance;
import com.badlogic.gdx.graphics.g3d.attributes.ColorAttribute;
import com.badlogic.gdx.graphics.g3d.environment.DirectionalLight;
import com.badlogic.gdx.graphics.g3d.utils.CameraInputController;
import com.badlogic.gdx.utils.Array;
public class MainGame implements ApplicationListener{
private PerspectiveCamera cam;
private CameraInputController camController;
private ModelBatch modelBatch;
private Array<ModelInstance> instances;
private Environment environment;
@Override
public void create() {
modelBatch = new ModelBatch();
environment = new Environment();
environment.set(new ColorAttribute(ColorAttribute.AmbientLight, 0.4f, 0.4f, 0.4f, 1f));
environment.add(new DirectionalLight().set(0.8f, 0.8f, 0.8f, -1f, -0.8f, -0.2f));
cam = new PerspectiveCamera(67, Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
cam.position.set(3f, 7f, 10f);
cam.lookAt(0, 4f, 0);
cam.update();
camController = new CameraInputController(cam);
Gdx.input.setInputProcessor(camController);
instances = new Array<ModelInstance>();
}
@Override
public void resize(int width, int height) {
}
@Override
public void render() {
camController.update();
Gdx.gl.glClearColor(0.3f, 0.3f, 0.3f, 1.f);
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT | GL20.GL_DEPTH_BUFFER_BIT);
modelBatch.begin(cam);
modelBatch.render(instances, environment);
modelBatch.end();
}
@Override
public void pause() {
}
@Override
public void resume() {
}
@Override
public void dispose() {
modelBatch.dispose();
}
}
private Model model;
private ModelInstance ground;
private ModelInstance ball;
在这里,使用 ModelBuilder 创建了一个有两个节点的模型。一个叫做“ ground”,另一个叫做“ ball”。接下来,我们为每个 Node 创建一个 ModelInstance (如本教程所述) ,并将球略微移到地面以上。
@Override
public void create() {
...
instances = new Array<ModelInstance>();
ModelBuilder mb = new ModelBuilder();
mb.begin();
mb.node().id = "ground";
MeshPartBuilder groundMeshPart = mb.part("box", GL20.GL_TRIANGLES, Usage.Position | Usage.Normal, new Material(ColorAttribute.createDiffuse(Color.RED)));
BoxShapeBuilder groundBoxMb = new BoxShapeBuilder();
groundBoxMb.build(groundMeshPart, 5f, 1f, 5f);
mb.node().id = "ball";
MeshPartBuilder ballMeshPart = mb.part("sphere", GL20.GL_TRIANGLES, Usage.Position | Usage.Normal, new Material(ColorAttribute.createDiffuse(Color.GREEN)));
SphereShapeBuilder sphereMb = new SphereShapeBuilder();
sphereMb.build(ballMeshPart, 1f, 1f, 1f, 10, 10);
model = mb.end();
ground = new ModelInstance(model, "ground");
ball = new ModelInstance(model, "ball");
ball.transform.setToTranslation(0, 9f, 0);
instances.add(ground);
instances.add(ball);
}
model.dispose();
运行效果如下:
现在我们要把球向下移动,直到它与地面碰撞。现在,我们新建一个方法 checkCollision 来作为碰撞检测。
private boolean checkCollision() {
return false;
}
然后在render中更新碰撞。
...
private boolean collision;
...
@Override
public void render() {
float delta = Math.min(1f/30f, Gdx.graphics.getDeltaTime());
if(!collision) {
ball.transform.translate(0f, -delta, 0f);
collision = checkCollision();
}
camController.update();
Gdx.gl.glClearColor(0.3f, 0.3f, 0.3f, 1.f);
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT | GL20.GL_DEPTH_BUFFER_BIT);
modelBatch.begin(cam);
modelBatch.render(instances, environment);
modelBatch.end();
}
这里添加了一个叫做碰撞的标志(collision),只要这个标志没有设置,我们就以每秒一个单位(例如米)的速度向下移动球。为此,我们使用 gdx.graphics.getdeltatiana ()方法,它给出了上次调用 render 方法以来的时间(以秒为单位)。Min 用于将此时间限制为最大值。这样可以避免“心灵运输”,例如当某些原因发生故障时。
现在运行代码,绿色的球会缓慢下落然后穿过红色的立方体接着掉下去。
你需要将 gdx-bullet 扩展添加到项目中。可以在使用gdx-setup新建项目勾选上。
也可以直接在 build.gradle 手动添加。
...
project(":desktop") {
apply plugin: "java-library"
dependencies {
implementation project(":core")
...
api "com.badlogicgames.gdx:gdx-bullet-platform:$gdxVersion:natives-desktop"
}
}
project(":android") {
apply plugin: "com.android.application"
configurations { natives }
dependencies {
implementation project(":core")
...
api "com.badlogicgames.gdx:gdx-bullet:$gdxVersion"
natives "com.badlogicgames.gdx:gdx-bullet-platform:$gdxVersion:natives-armeabi-v7a"
natives "com.badlogicgames.gdx:gdx-bullet-platform:$gdxVersion:natives-arm64-v8a"
natives "com.badlogicgames.gdx:gdx-bullet-platform:$gdxVersion:natives-x86"
natives "com.badlogicgames.gdx:gdx-bullet-platform:$gdxVersion:natives-x86_64"
}
}
project(":core") {
apply plugin: "java-library"
dependencies {
...
api "com.badlogicgames.gdx:gdx-bullet:$gdxVersion"
}
}
Bullet 包装器使用第一步就是要:
@Override
public void create () {
Bullet.init();
...
}
在我们检查球是否与地面碰撞之前,我们需要指定两个物体的形状。Bullet 提供了许多碰撞形状。从原始形状,如盒子,球体,圆柱体,圆锥体和胶囊,到一个更一般的凸形状,一个优化的凸壳形状,网格形状和它们的任何组合。现在,一个简单的盒子和球体就足够我们进行测试了:
public MainGame implements ApplicationListener{
...
private boolean collision;
private btCollisionShape groundShape;
private btCollisionShape ballShape;
...
@Override
public void create() {
Bullet.init();
...
ModelBuilder mb = new ModelBuilder();
mb.begin();
mb.node().id = "ground";
MeshPartBuilder groundMeshPart = mb.part("box", GL20.GL_TRIANGLES, Usage.Position | Usage.Normal, new Material(ColorAttribute.createDiffuse(Color.RED)));
BoxShapeBuilder groundBoxMb = new BoxShapeBuilder();
groundBoxMb.build(groundMeshPart, 5f, 1f, 5f);
groundShape = new btBoxShape(new Vector3(2.5f, 0.5f, 2.5f));
mb.node().id = "ball";
MeshPartBuilder ballMeshPart = mb.part("sphere", GL20.GL_TRIANGLES, Usage.Position | Usage.Normal, new Material(ColorAttribute.createDiffuse(Color.GREEN)));
SphereShapeBuilder sphereMb = new SphereShapeBuilder();
sphereMb.build(ballMeshPart, 1f, 1f, 1f, 10, 10);
ballShape = new btSphereShape(0.5f);
model = mb.end();
ground = new ModelInstance(model, "ground");
ball = new ModelInstance(model, "ball");
ball.transform.setToTranslation(0, 9f, 0);
instances.add(ground);
instances.add(ball);
}
}
btCollisionShape 是每个形状的基类。对于球,我们创建一个 btSphereShape,它以半径为参数。球的直径是1个单位,因此半径是0.5 f。对于地面,我们创建一个 btBoxShape,它接受一半的外延作为参数。因为地面是5个单元宽,1个单元高,有5个单元深,我们需要提供这些数值的一半。这是用 vector3完成的。
注意 vector3是一个 LibGDX 类。Bullet 的等价物是 btVector3(它也可以在包装器中使用)。只要有可能,包装器将在 LibGDX 的数学类和 Bullet 数学类之间架起桥梁。在这样做的时候,它甚至会为您做一些优化(稍后将详细介绍)。
一个碰撞的形状是不足以做碰撞检测的。我们需要告知 Bullet 的位置(和旋转)的每个形状。这是通过使用一个碰撞对象完成的:
public class MainGame implements ApplicationListener {
...
private btCollisionObject groundObject;
private btCollisionObject ballObject;
@Override
public void create () {
...
groundObject = new btCollisionObject();
groundObject.setCollisionShape(groundShape);
groundObject.setWorldTransform(ground.transform);
...
ballObject = new btCollisionObject();
ballObject.setCollisionShape(ballShape);
ballObject.setWorldTransform(ball.transform);
}
@Override
public void render () {
...
if (!collision) {
ball.transform.translate(0f, -delta, 0f);
ballObject.setWorldTransform(ball.transform);
collision = checkCollision();
}
...
}
...
}
碰撞对象只是碰撞形状和它的变换的组合。在这种情况下,变换是它的位置和旋转。就像我们使用 vector3 处理盒子形状一样,我们可以使用 ModelInstance 的转换成员来设置转换。包装器将这个 matrix4转换为 Bullet 的等效 btTransform。虽然这很容易操作,但是您应该记住,转换(就Bullet而言)只包含一个位置和旋转。不支持任何其他转换,例如缩放。实际上,这意味着在使用Bullet包装器时,绝不应该将缩放直接应用于对象。还有其他缩放对象的方法,但总的来说,建议尽量避免缩放。
现在我们得到了两个物体,我们想要检测它们是否碰撞。在我们开始实际的碰撞检测之前,我们需要一些帮助类。
public class MainGame implements ApplicationListener{
...
private btCollisionConfiguration collisionConfig;
private btDispatcher dispatcher;
@Override
public void create() {
...
collisionConfig = new btDefaultCollisionConfiguration();
dispatcher = new btCollisionDispatcher(collisionConfig);
}
...
}
稍后我们将讨论这些对象的重要性,现在只需确保在 create 方法中构造它们。到目前为止,所有 Bullet 类都是以前缀“ bt”开头的。虽然这并不总是正确的,但在大多数情况下是正确的。每次在 java 中构造 Bullet 类时,包装器还将在本机(c++)库中构造相同的类。但是在 java 中,垃圾收集器负责内存管理,当你不再使用对象时,它会释放对象,而在 c++ 中,你自己负责释放内存。您可能已经熟悉了这个操作,因为对于纹理、模型、模型批处理、着色器等也是如此。因此,当您不再需要该对象时,必须手动释放该对象。
@Override
public void dispose () {
groundObject.dispose();
groundShape.dispose();
ballObject.dispose();
ballShape.dispose();
dispatcher.dispose();
collisionConfig.dispose();
modelBatch.dispose();
model.dispose();
}
现在我们可以开始实现 checkCollision 方法了。基本上我们需要检查的是球体是否与盒子碰撞。有一个专门的算法,叫做 btSphereBoxCollisionAlgorithm。
private boolean checkCollision() {
CollisionObjectWrapper co0 = new CollisionObjectWrapper(ballObject);
CollisionObjectWrapper co1 = new CollisionObjectWrapper(groundObject);
btCollisionAlgorithmConstructionInfo ci = new btCollisionAlgorithmConstructionInfo();
ci.setDispatcher1(dispatcher);
btCollisionAlgorithm algorithm = new btSphereBoxCollisionAlgorithm(null, ci, co0.wrapper, co1.wrapper, false);
btDispatcherInfo info = new btDispatcherInfo();
btManifoldResult result= new btManifoldResult(co0.wrapper, co1.wrapper);
algorithm.processCollision(co0.wrapper, co1.wrapper, info, result);
boolean r = result.getPersistentManifold().getNumContacts() > 0;
result.dispose();
info.dispose();
algorithm.dispose();
ci.dispose();
co1.dispose();
co0.dispose();
return r;
}
第一步为每个对象创建一个碰撞对象包装器CollisionObjectWrapper。请注意,这个类不以前缀“ bt”开头,这是因为它是一个特定于 libgdx Bullet 包装器的类(它包装了 btCollisionObjectWrapper 对象,可以通过包装器成员访问该对象)。
接下来我们构造一个 btCollisionAlgorithmConstructionInfo 实例,它用于指定有关我们要创建的碰撞算法的信息。我们将保持默认设置,因为它需要 btDispatcher,所以我们只向它提供前面创建的 dispatcher。
然后我们构造一个 btSphereBoxCollisionAlgorithm,它需要我们刚刚创建的对象作为构造函数的参数。我们将使用这个算法来检查球体是否与盒子相撞。为了执行这个算法,我们需要一个附加的 btDispatcherInfo (它提供关于所需算法的附加信息)和一个 btManifoldResult (它将接收结果)。注意,该算法是 btCollisionAlgorithm 的一个实例,btCollisionAlgorithm 是所有碰撞算法中的超类。
接下来我们可以通过调用算法的 processCollision 方法来实际执行该算法。这将在我们前面创建的 btManifoldResult 中存储结果(联系点)。如果接触点的数量大于零,就会发生碰撞。
最后,正如我们前面所看到的,处置构造函数中的每个 Bullet 类是非常重要的。
运行目前的代码,小球掉落下去就会停留在方块上了。
总的来说我们创建了两个碰撞形状。然后创建两个包含形状、位置和旋转的碰撞对象。为了检查两个物体是否碰撞,我们创建了一个专门用于检测球盒碰撞的碰撞算法。这个碰撞检测的结果被称为流形,其中包含碰撞的接触点(如果有的话)。这些接触点包含有关碰撞的信息,例如碰撞的距离(穿透)和方向。
Bullet 有许多碰撞算法,对于每一对可能的碰撞对象(形状) ,都有一个碰撞算法。为每个碰撞对象手工创建一个碰撞算法将会很快变得混乱。幸运的是 Bullet 可以为我们创建一个每对物体的碰撞算法。这是由我们前面创建的调度器(dispatcher)完成的。
@Override
public void render () {
...
collision = checkCollision(ballObject, groundObject);
...
}
boolean checkCollision(btCollisionObject obj0, btCollisionObject obj1) {
CollisionObjectWrapper co0 = new CollisionObjectWrapper(obj0);
CollisionObjectWrapper co1 = new CollisionObjectWrapper(obj1);
btCollisionAlgorithm algorithm = dispatcher.findAlgorithm(co0.wrapper, co1.wrapper, null, 0);
btDispatcherInfo info = new btDispatcherInfo();
btManifoldResult result = new btManifoldResult(co0.wrapper, co1.wrapper);
algorithm.processCollision(co0.wrapper, co1.wrapper, info, result);
boolean r = result.getPersistentManifold().getNumContacts() > 0;
dispatcher.freeCollisionAlgorithm(algorithm.getCPointer());
result.dispose();
info.dispose();
co1.dispose();
co0.dispose();
return r;
}
我稍微修改了 checkCollision 方法,以便它可以用于任何一对碰撞对象。现在,我们不再手动创建球盒碰撞算法,而是要求调度程序使用 dispatcher.findAlgorithm 方法为我们找到正确的算法。这个方法的其余部分和之前的几乎一样。除了一件事: 我们不再拥有这个算法,所以我们不必再处理它了。相反,我们需要通知调度程序我们已经完成了算法,这样它就可以在其他碰撞侦测重用(合用)。为此,调度程序现在需要在内存中记录算法的位置。正如我们在前面看到的,我们可以使用 getCPointer 方法来获得这个位置。
我们现在有一个通用的方法来检查两个对象是否碰撞。因此,让我们添加一些更多的对象,我们可以相互比较。同时,我们也可以使代码在使用多个对象时更加简洁。因此,首先通过扩展 ModelInstance 类将碰撞的对象添加到模型实例中:
public class MainGame implements ApplicationListener{
static class GameObject extends ModelInstance implements Disposable {
public final btCollisionObject body;
public boolean moving;
public GameObject(Model model, String node, btCollisionShape shape) {
super(model, node);
body = new btCollisionObject();
body.setCollisionShape(shape);
}
@Override
public void dispose () {
body.dispose();
}
}
...
}
通过使用 btCollisionObject 主体成员(主要是碰撞形状和转换) ,可以更容易地维护我们的游戏对象。我们将使用移动成员来决定如果对象是否在地面上。
另一个让代码变得简洁的好方法是使用“工厂”类:
static class GameObject extends ModelInstance implements Disposable {
...
static class Constructor implements Disposable {
public final Model model;
public final String node;
public final btCollisionShape shape;
public Constructor(Model model, String node, btCollisionShape shape) {
this.model = model;
this.node = node;
this.shape = shape;
}
public GameObject construct() {
return new GameObject(model, node, shape);
}
@Override
public void dispose () {
shape.dispose();
}
}
}
现在我们可以有一个游戏对象。构造函数,并调用其上的构造方法来创建一个 GameObject。如果你把这个和地图结合起来,你会得到一个非常方便的方法来构建你的游戏对象:
public class MainGame implements ApplicationListener {
...
Array<GameObject> instances;
ArrayMap<String, GameObject.Constructor> constructors;
@Override
public void create () {
...
camController = new CameraInputController(cam);
Gdx.input.setInputProcessor(camController);
ModelBuilder mb = new ModelBuilder();
mb.begin();
mb.node().id = "ground";
mb.part("ground", GL20.GL_TRIANGLES, Usage.Position | Usage.Normal, new Material(ColorAttribute.createDiffuse(Color.RED)))
.box(5f, 1f, 5f);
mb.node().id = "sphere";
mb.part("sphere", GL20.GL_TRIANGLES, Usage.Position | Usage.Normal, new Material(ColorAttribute.createDiffuse(Color.GREEN)))
.sphere(1f, 1f, 1f, 10, 10);
mb.node().id = "box";
mb.part("box", GL20.GL_TRIANGLES, Usage.Position | Usage.Normal, new Material(ColorAttribute.createDiffuse(Color.BLUE)))
.box(1f, 1f, 1f);
mb.node().id = "cone";
mb.part("cone", GL20.GL_TRIANGLES, Usage.Position | Usage.Normal, new Material(ColorAttribute.createDiffuse(Color.YELLOW)))
.cone(1f, 2f, 1f, 10);
mb.node().id = "capsule";
mb.part("capsule", GL20.GL_TRIANGLES, Usage.Position | Usage.Normal, new Material(ColorAttribute.createDiffuse(Color.CYAN)))
.capsule(0.5f, 2f, 10);
mb.node().id = "cylinder";
mb.part("cylinder", GL20.GL_TRIANGLES, Usage.Position | Usage.Normal, new Material(ColorAttribute.createDiffuse(Color.MAGENTA)))
.cylinder(1f, 2f, 1f, 10);
model = mb.end();
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))));
constructors.put("sphere", new GameObject.Constructor(model, "sphere", new btSphereShape(0.5f)));
constructors.put("box", new GameObject.Constructor(model, "box", new btBoxShape(new Vector3(0.5f, 0.5f, 0.5f))));
constructors.put("cone", new GameObject.Constructor(model, "cone", new btConeShape(0.5f, 2f)));
constructors.put("capsule", new GameObject.Constructor(model, "capsule", new btCapsuleShape(.5f, 1f)));
constructors.put("cylinder", new GameObject.Constructor(model, "cylinder", new btCylinderShape(new Vector3(.5f, 1f, .5f))));
instances = new Array<GameObject>();
instances.add(constructors.get("ground").construct());
collisionConfig = new btDefaultCollisionConfiguration();
dispatcher = new btCollisionDispatcher(collisionConfig);
}
...
@Override
public void dispose () {
for (GameObject obj : instances)
obj.dispose();
instances.clear();
for (GameObject.Constructor ctor : constructors.values())
ctor.dispose();
constructors.clear();
dispatcher.dispose();
collisionConfig.dispose();
modelBatch.dispose();
model.dispose();
}
}
我已经移除了前面的代码来构造碰撞形状和对象,并用 GameObject.Constructor 替换了它。instances 数组现在是一个 GameObject 实例数组。我们使用 ModelBuilder 为每个形状创建节点。接下来我们创建一个 GameObject.Constructor,包括每个构造函数的 btCollisionShape。我们已经在 map 中为每个构造函数赋予了一个描述性名称,因此您现在可以创建一个类似于这样的游戏对象: constructors.get(name)。当然,我们需要像前面一样处理每个碰撞对象和形状,因此对处理方法也进行了一些修改。现在让我们修改渲染方法来使用 GameObject,让它每隔一秒左右添加一个新的游戏对象:
public class MainGame implements ApplicationListener {
...
private float spawnTimer;
...
@Override
public void render () {
final float delta = Math.min(1f/30f, Gdx.graphics.getDeltaTime());
for (GameObject obj : instances) {
if (obj.moving) {
obj.transform.trn(0f, -delta, 0f);
obj.body.setWorldTransform(obj.transform);
if (checkCollision(obj.body, instances.get(0).body))
obj.moving = false;
}
}
if ((spawnTimer -= delta) < 0) {
spawn();
spawnTimer = 1.5f;
}
...
}
public void spawn() {
GameObject obj = constructors.values[1+MathUtils.random(constructors.size-2)].construct();
obj.moving = true;
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);
instances.add(obj);
}
...
}
对于每个游戏对象,如果moving是ture,则每秒移动一个单位。当他与第一个游戏对象发生碰撞时将moving设置为false, 停止移动。接下来,我们随机构造一个新的游戏对象(除了地板) ,并将其设置为地面上的一个随机位置。我们也随机地旋转它。因为物体现在是旋转的,所以我们使用 trn 方法来代替 translate 方法来转换物体。Trn 方法无论对象是否旋转,都会将其平移,因此对象总是朝着我们指定的位置移动。运行代码,效果如下:
上面的代码如果只是对于想检测两个物体是否碰撞的话那问题不大。但是如果我们还想检测物体是否相互碰撞,物体的数量是否增加,那上面的代码就有很大的问题了。与检测每个可能的碰撞对不同,在碰撞发生时获得通知要方便得多。幸运的是 Bullet 提供了ContactListener监听器。
public class MainGame implements ApplicationListener {
class MyContactListener extends ContactListener {
@Override
public boolean onContactAdded (btManifoldPoint cp, btCollisionObjectWrapper colObj0Wrap, int partId0, int index0,
btCollisionObjectWrapper colObj1Wrap, int partId1, int index1) {
instances.get(colObj0Wrap.getCollisionObject().getUserValue()).moving = false;
instances.get(colObj1Wrap.getCollisionObject().getUserValue()).moving = false;
return true;
}
}
...
MyContactListener contactListener;
@Override
public void create () {
...
contactListener = new MyContactListener();
}
@Override
public void render () {
...
for (GameObject obj : instances) {
if (obj.moving) {
obj.transform.trn(0f, -delta, 0f);
obj.body.setWorldTransform(obj.transform);
checkCollision(obj.body, instances.get(0).body);
}
}
...
}
public void spawn() {
...
obj.body.setUserValue(instances.size);
obj.body.setCollisionFlags(obj.body.getCollisionFlags() | btCollisionObject.CollisionFlags.CF_CUSTOM_MATERIAL_CALLBACK);
...
}
...
@Override
public void dispose () {
...
contactListener.dispose();
...
}
}
这里我们创建一个 ContactListener,它不是 Bullet 类,而是专门为包装器创建的类。Bullet 对冲突事件不使用面向对象的回调,所有回调都是全局方法(有点类似于静态 java 方法)。因为在 Java 中不可能使用全局回调方法,所以包装器添加 ContactListener 类来处理这个问题。这也是为什么我们不必通知 bullet 来使用 ContactListener,在构造 ContactListener 时,包装器会处理这个问题。
每当一个接触点被添加到流形上时,onContactAdded 方法就会被调用。正如我们之前看到的,一旦两个物体有一个或多个接触点,就会发生碰撞。所以基本上,当两个物体发生碰撞时,这个方法就被调用了。
当然 Bullet 库不知道我们的 GameObject 类,所以我们需要一种方法来使用 Bullet 在 callback 方法中提供给我们的数据来获得我们的 GameObject。如果您熟悉 box2d,那么您可能也熟悉使用 userData 成员进行此操作。Bullet 包装器还支持 btCollisionObject 的 userData 成员,该成员实际上是相同的。但是,我们将使用 setUserValue 和 getUserValue 方法。这是一个整数值,我们在 instances 数组中将其设置为 GameObject 的索引。因此,使用 instances.get(colObj0Wrap.getCollisionObject().getUserValue()) 我们可以得到相应的游戏对象。
在 spawn 方法中,我们使用 setUserValue 方法将这个值设置为 instances 数组中对象的索引。我们还通知 Bullet,我们希望通过添加CF_CUSTOM_MATERIAL_CALLBACK 标志来接收这个对象的冲突事件。要调用 onContactAdded 方法,需要使用此标志。
在 render 方法中,我们现在不再需要设置移动标志,所以我删除了那部分,只调用 checkCollision 方法。与以往一样,必须释放 contactListener,因此我们向 dispose 方法添加了一行。
运行代码效果是一样的。
前面我们已经看到,所有的包装类基本上都是指向相应的 c++ 对象的指针。这就是当您在 java 对象上调用方法时,包装器如何在 c++ 对象上调用适当的方法。但这只是一条单行道。只有专门为扩展而设计的类才会调用重写的方法。这是为了减少从 c++ 到 Java 的桥接开销,以便在不打算重写方法时不会损失性能。ContactListener 就是这样一个类,它打算被重写,而且还有相当多的其他“回调”类。
正如我们在前面看到的,包装器将根据需要为每个 c++ 对象创建一个 java 对象。在不需要的时候创建对象当然是我们想要避免的(尤其是在游戏中)。因此,包装器允许您指定 ContactListener 方法中真正需要的对象。为此,ContactListener 具有同一方法的多个签名,您可以重写这些签名。您只能重写其中的一个,因为包装器只会为一个事件调用一个方法。
下面的ContactListener,我们从不使用 btManifoldPoint。因此,如果我们使用不包含参数的签名,那么包装器就不必创建它:
class MyContactListener extends ContactListener {
@Override
public boolean onContactAdded (btCollisionObjectWrapper colObj0Wrap, int partId0, int index0,
btCollisionObjectWrapper colObj1Wrap, int partId1, int index1) {
instances.get(colObj0Wrap.getCollisionObject().getUserValue()).moving = false;
instances.get(colObj1Wrap.getCollisionObject().getUserValue()).moving = false;
return true;
}
}
因为 btCollisionObjectWrapper 经常在回调中使用,所以包装器会特别注意这一点。它为这些对象使用一个池。但是由于我们实际上并不使用 btCollisionObjectWrapper,而只需要它包装的 btCollisionObject,我们不妨使用提供 btCollisionObject 的方法签名。
class MyContactListener extends ContactListener {
@Override
public boolean onContactAdded (btCollisionObject colObj0, int partId0, int index0, btCollisionObject colObj1, int partId1,
int index1) {
instances.get(colObj0.getUserValue()).moving = false;
instances.get(colObj1.getUserValue()).moving = false;
return true;
}
}
由于 btCollisionObject 始终是在 Java 中创建的,所以包装器将确保在每次需要时使用该实例。为此,它使用了一个长映射(以 c++ 指针为键)。当然,查找正确的 btCollisionObject 会有一些开销,但这允许您扩展 btCollisionObject,并确保您始终可以访问回调函数中的扩展类。
但是,我们只需要回调中的用户值,这个值足以在实例数组中定位游戏对象。我们不需要包装器来使用 long-map 查找冲突对象。
class MyContactListener extends ContactListener {
@Override
public boolean onContactAdded (int userValue0, int partId0, int index0, int userValue1, int partId1, int index1) {
instances.get(userValue0).moving = false;
instances.get(userValue1).moving = false;
return true;
}
}
包装器能够为我们提供 userValue,从而完全不需要创建对象。我们现在已经创建了一个只有基元参数的回调方法,这意味着不会为这个方法调用创建对象或映射查找。
现在我们接收到一个碰撞事件,但是我们仍然手动检查每个物体是否与地面碰撞。虽然我们用来检查碰撞的方法工作得很好,但是它还远远没有优化。首先,我们使用 checkCollision 方法构造和销毁了相当多的对象。正如我们刚才看到的,最好是防止频繁地创建对象。否则,垃圾收集器可能每隔几秒钟左右就会引起一次故障。
但是即使我们构造了很多对象,还有另外一个问题。我们每次都使用专门的碰撞算法。专门的碰撞算法相对昂贵。理想情况下,我们应该首先检查两个对象是否相邻,例如使用边界框或边界球。而且只有当它们彼此靠近时,我们才会使用更精确的专门的碰撞算法。
使用这样的两阶段碰撞检测有很多好处。第一阶段,我们发现相互靠近的碰撞物体,被称为广义阶段。然后第二个阶段,使用一个更精确的专门的碰撞算法,被称为近期阶段。到目前为止,我们只研究了近期阶段。实际上,冲突调度程序就是我们在接近阶段使用的类。
正如您可以想象的那样,在一个常见的场景中,对所有碰撞对象调用广义阶段,而对少数对象调用近似阶段。因此,高度优化广义相位是至关重要的。Bullet 通过缓存冲突信息来做到这一点,因此它不必每次都重新计算冲突信息。有几种实现可供选择,但实际上这是以树的形式完成的。更多关于它的信息,你可以搜索“轴对齐的边界框树”或者简称为“ AABB 树”。
术语“ AABB”经常在碰撞检测中使用(与广义阶段有关)。这仅仅是指边界框,就像我们在前面的几个教程中使用的。边界只包括一个位置(中心)和它的尺寸。它不包含旋转,这使得检查两个边界框是否重叠变得非常容易(和便宜)。
当然,树必须存储在某个地方,并在添加、删除或转换对象时进行更新。幸运的是 Bullet 为我们提供了一个很好的类,叫做碰撞世界。基本上,你告诉世界你想使用哪个广义阶段和接近阶段,接下来你可以添加,删除或转换碰撞对象,碰撞世界会通知你(通过 ContactListener)当碰撞发生。让我们添加一个碰撞的世界,包括一个广泛的阶段:
public class MainGame implements ApplicationListener {
...
private btBroadphaseInterface broadphase;
private btCollisionWorld collisionWorld;
@Override
public void create () {
...
collisionConfig = new btDefaultCollisionConfiguration();
dispatcher = new btCollisionDispatcher(collisionConfig);
broadphase = new btDbvtBroadphase();
collisionWorld = new btCollisionWorld(dispatcher, broadphase, collisionConfig);
contactListener = new MyContactListener();
instances = new Array<GameObject>();
GameObject object = constructors.get("ground").construct();
instances.add(object);
collisionWorld.addCollisionObject(object.body);
}
public void spawn () {
...
instances.add(obj);
collisionWorld.addCollisionObject(obj.body);
}
@Override
public void render () {
final float delta = Math.min(1f / 30f, Gdx.graphics.getDeltaTime());
for (GameObject obj : instances) {
if (obj.moving) {
obj.transform.trn(0f, -delta, 0f);
obj.body.setWorldTransform(obj.transform);
}
}
collisionWorld.performDiscreteCollisionDetection();
...
}
// Remove the checkCollision method, it's no longer needed
@Override
public void dispose () {
...
collisionWorld.dispose();
broadphase.dispose();
...
}
...
}
在这里我们创建了 btBroadphaseInterface 和 btCollisionWorld。对于广义的阶段,我选择了 btDbvtBroadphase 实现,它是一个动态包围卷树实现。在大多数情况下,这种实现应该足够了。接下来,我们需要使用 collisionWorld.addCollisionObject 方法将 spawn 方法中创建的 ground 和其他对象添加到碰撞世界中。我已经删除了 checkCollision 方法,现在我们改为调用 collisionworld.performdiscretecolliiondetection () 。这个方法将检查我们添加到这个世界中的所有对象之间的冲突,并在发生冲突时调用 ContactListener。
再次运行代码,效果也还是一样的。
因为这个世界可以探测到所有物体之间的碰撞,而不仅仅是地面上的碰撞,所以当物体相互碰撞时,它们也会停止运动,解决这个问题代码如下。
class MyContactListener extends ContactListener {
@Override
public boolean onContactAdded (int userValue0, int partId0, int index0, int userValue1, int partId1, int index1) {
if (userValue1 == 0)
instances.get(userValue0).moving = false;
else if (userValue0 == 0)
instances.get(userValue1).moving = false;
return true;
}
}
我们知道 ground 的 userValue 为零。现在我们检查碰撞的物体中是否有一个是地面,如果有的话,我们就停止移动另一个物体。因为其中一个对象可能是地,所以我们需要检查这两个值。虽然这对我们的测试有用,但它不是一个非常通用的解决方案。更重要的是,世界仍然需要对我们想要忽略的对进行广义相位和近相位碰撞侦测。更好的做法是告诉世界,它可以简单地忽略物体之间的相互对抗,只需要检查每个物体是否与地面碰撞。一个非常有效的方法是使用碰撞标志:
public class MainGame implements ApplicationListener {
final static short GROUND_FLAG = 1 << 8;
final static short OBJECT_FLAG = 1 << 9;
final static short ALL_FLAG = -1;
...
@Override
public void create () {
...
collisionWorld.addCollisionObject(object.body, GROUND_FLAG, ALL_FLAG);
}
public void spawn () {
...
collisionWorld.addCollisionObject(obj.body, OBJECT_FLAG, GROUND_FLAG);
}
}
这里我们创建了三各标志。第一个 GROUND_FLAG 只设置了第九位。第二个 OBJECT_FLAG 只设置了第十位。最后一个 ALL_FLAG 已经设置了所有位。接下来,当我们将地面添加到这个世界中时,我们告诉 Bullet 这个特定的物体使用哪个标志,以及这个物体可以与哪个物体碰撞。所以地面会碰撞所有的物体。当产生一个物体时,我们告诉 Bullet 物体只能与地面碰撞。Bullet 使用按位比较来检查它是否应该检测两个对象之间的碰撞。