上一章完整的介绍了BOX2D的物理仿真创建过程,在本章我们将继续完成BOX2D的剩余内容——碰撞检测。因为BOX2D帮我们完成了所有物理模拟过程,包括碰撞检测,这极大的降低我们的项目难度,我们不需要理解碰撞检测如何运行,甚至不需要知道任何碰撞检测算法,就能够完成碰撞检测并通知我们。下面就让我们为FlappyBird添加碰撞检测的回调函数和相应的逻辑代码。 碰撞检测 打开WorldController类,并为其添加下面代码:
...
public class WorldController extends InputAdapter implements Disposable {
...
private void initWorld() {
if(world != null) world.dispose();
world = new World(new Vector2(0, -110.8f), false);
// 为world添加碰撞检测监听器
world.setContactListener(new BirdContactListener());
}
...
private void collisionDetection(AbstractGameObject a, AbstractGameObject b) {
}
// 碰撞检测监听器
private class BirdContactListener implements ContactListener {
@Override public void beginContact(Contact arg0) {
AbstractGameObject a = (AbstractGameObject)arg0.getFixtureA().getBody().getUserData();
AbstractGameObject b = (AbstractGameObject)arg0.getFixtureB().getBody().getUserData();
collisionDetection(a, b);
}
@Override public void endContact(Contact arg0) {}
@Override public void postSolve(Contact arg0, ContactImpulse arg1) {}
@Override public void preSolve(Contact arg0, Manifold arg1) {}
}
}
首先定义一个BirdContactListener类,该类实现了ContactListener接口的四个方法,beginContact()和endContact()分别在开始碰撞(重叠)和结束碰撞时调用。preSolve()方法在碰撞检测之后,解决碰撞之前调用,你可以实现该方法替代BOX2D内置的碰撞处理方式;最后一个postSolve()方法在碰撞发生后调用,该方法是获得碰撞冲量的地方。这里只使用了一个方法beginContact(),该方法内首先通过Contact获得两个碰撞对象的Fiture然后再获得Body对象,因为创建Body时调用setUserData()方法将游戏对象本身作为用户数据,所有这里我们便可直接通过getUserData()获得游戏对象。最后调用WorldController()私有方法collisionDetection()处理碰撞事件。
在初始化函数init()中创建了一个BirdContactListener对象并调用World.setContactListener()为world设置了碰撞监听器。
根据上述添加的代码可以推断,一旦发生碰撞,collisionDetection()方法就会被调用,现在我们需要考虑在collisionDetection做什么?我们知道,FlappyBird游戏非常简单,一旦发生碰撞,游戏就即将结束,所以这里我们应该通知WorldController和Bird发生了碰撞,游戏需要马上结束。WorldController收到碰撞消息应该立马停止BOX2D模拟,Bird收到消息应该开始完成结束动画。
接下来,首先修改Bird对象:
...
public class Bird extends AbstractGameObject {
...
protected static final float DIED_DROP_DRAG = 0.5f; // 死亡后下落速度
...
private boolean contacted;
private boolean flashed;
...
// 初始化
public void init(int selected) {
contacted = false;
flashed = false;
...
}
...
// 通知碰撞事件
public void contact() {
contacted = true;
}
public boolean isContact() {
return contacted;
}
...
@Override
public void render(SpriteBatch batch) {
// 如果发生碰撞则不再播放帧动画
if(!contacted) {
animDuration += Gdx.graphics.getDeltaTime();
currentFrame = birdAnimation.getKeyFrame(animDuration);
}
// 发生碰撞但游戏还没有结束时小鸟要进行落地动画
else {
position.y -= DIED_DROP_DRAG;
rotation -= BIRD_FLAP_ANGLE_POWER;
position.y = Math.max(position.y, Land.LAND_HEIGHT - Constants.VIEWPORT_HEIGHT / 2 + dimension.y / 2);
rotation = Math.max(rotation, BIRD_MAX_DROP_ANGLE);
}
batch.draw(currentFrame.getTexture(), position.x - dimension.x / 2, position.y - dimension.y / 2,
dimension.x / 2, dimension.y / 2, dimension.x, dimension.y, scale.x, scale.y, rotation,
currentFrame.getRegionX(), currentFrame.getRegionY(), currentFrame.getRegionWidth(),
currentFrame.getRegionHeight(), false, false);
if (contacted && !flashed) {
flashed = true;
float w = Gdx.graphics.getWidth();
float h = Gdx.graphics.getHeight();
batch.draw(Assets.instance.decoration.white, -w / 2, -h / 2, w, h);
}
}
}
首先添加了一个DIED_DROP_DRAG表示小鸟发生碰撞之后的下落速度,然后添加了两个boolean类型变量,其中contacted表示小鸟是否发生碰撞,flashed表示是否完成碰撞后的闪烁。后面我们添加了两个方法分别用于设置和获得contacted变量。变化最大的就是render()方法,通过对代码分解便可理解,如果没有发生碰撞,则一切正常执行,如果发生碰撞,则保持最后一次获得的动画帧不在更新,接着根据DIED_DROP_DRAG下降速度更新小鸟的y坐标,并根据BIRD_FLAP_ANGLE_POWER旋转速度更新小鸟的旋转角度,最后限定y坐标和rotation的最小值。render()方法最后还根据contacted和flashed完成闪烁过程,闪烁原理其实很简单,一旦发生碰撞我们就是用白色图片填充场景一帧即可。
接下来修改WorldController:
...
public class WorldController extends InputAdapter implements Disposable {
...
public void update(float deltaTime) {
if(bird.isContact()) return;
...
}
...
private void collisionDetection(AbstractGameObject a, AbstractGameObject b) {
if (a instanceof Bird) {
((Bird) a).contact();
} else if (b instanceof Bird) {
((Bird) b).contact();
}
Gdx.app.debug(TAG, "Player Character Contected!");
}
...
}
首先在update()方法中测试bird对象是否发生碰撞,如果发生碰撞则不在跟新。接着修改collisionDetection()方法,一旦发生碰撞则通知Bird对象。
游戏结束根据前面的分析我们可以确定,当Bird发生碰撞后,游戏并没有立即结束,还要进行结束前的一些动画(如果闪烁、降落)过程。接下来我们将为游戏添加结束过程。
首先我们要确定游戏结束的条件。根据前面分析,当Bird对象发生碰撞后,需要完成下落动画才能结束游戏,因此游戏结束的标志就是小鸟已经完成下落动画,即小鸟的y轴坐标到达了地面且旋转角度rotation等于了-90度,所以为Bird添加判断是否游戏结束方法isGameOver():
public boolean isGameOver() {
return contacted &&
(position.y <= Land.LAND_HEIGHT -
Constants.VIEWPORT_HEIGHT / 2 + dimension.y / 2) &&
(rotation == BIRD_MAX_DROP_ANGLE);
}
接下来为WorldController添加一个boolean类型变量isGameOver,然后在update()中判断游戏是否结束:
...
public class WorldController extends InputAdapter implements Disposable {
...
public boolean isGameOver;
public WorldController() {
init();
}
private void init() {
...
isStart = false;
isGameOver = false;
...
}
...
public void update(float deltaTime) {
if(!isGameOver) {
isGameOver = bird.isGameOver();
}
if(bird.isContact()) return;
...
}
...
}
既然游戏结束了那么游戏就可以重新启动。下面修改WorldController.touchDown()方法让游戏可以重新开始:
@Override
public boolean touchDown(int screenX, int screenY, int pointer, int button) {
if (button == Buttons.LEFT) {
if(!isStart) {
isStart = true;
bird.beginToSimulate(world);
land.beginToSimulate(world);
}
if(isGameOver) {
init();
}
bird.setJumping();
}
return true;
}
上述代码判断,当鼠标按下或发生触摸屏幕事件,我们判断游戏是否结束,如果结束则调用init()重新开始。但是要让上面修改可以正常启动,我们还必须作出重大修改:
首先为AbstractGameObject添加init()方法,并将构造函数的所有内容移入该方法内:
public AbstractGameObject() {
}
public void init() {
position = new Vector2();
dimension = new Vector2(1, 1);
origin = new Vector2();
scale = new Vector2(1, 1);
rotation = 0;
body = null;
}
接着修改三个对象的init()方法,分别保证对父类的init()方法调用:
public Bird() {
init((int) (Math.random() * 3));
}
// 初始化
public void init(int selected) {
super.init();
...
}
public Land() {
...
init();
}
public void init() {
super.init();
...
}
public Pipe() {
...
init();
}
public void init() {
super.init();
...
}
现在我们就可以测试应用了:
现在我们已经完成了游戏的一个完整过程,并且可以重新启动,但是还缺少许多GUI和开始结束信息。这些内容将在下一章介绍。现在让我们来处理一个细节,因为在运行过程中每经过一定时间就会创建一个Pipe,而Pipe对象包含一个Body对象,所以如果当一个Pipe对象不在需要的时候我们不及时释放对应的Body对象,时间一长将大大降低BOX2D的运行效率,所以我们需要修改Pipes和Pipe类,使得运行更佳流畅。
细节处理
经过上述分析,我们修改Pipes类:
...
public class Pipes extends AbstractGameObject {
...
private void testPipeNumberIsTooLarge(int amount) {
if (pipes != null && pipes.size > amount) {
pipes.get(0).destroy();
pipes.removeIndex(0);
}
}
...
public class Pipe extends AbstractGameObject {
...
@Override
public void beginToSimulate(World world) {
// down
BodyDef bodyDef = new BodyDef();
bodyDef.type = BodyType.KinematicBody;
bodyDef.position.set(position);
body = world.createBody(bodyDef);
PolygonShape shape = new PolygonShape();
shape.setAsBox(dimension.x / 2, dnPipeHeight / 2,
new Vector2(dimension.x / 2, dnPipeHeight / 2), 0);
shape.setRadius(-0.1f);
FixtureDef fixtureDefDown = new FixtureDef();
fixtureDefDown.shape = shape;
body.createFixture(fixtureDefDown);
body.setLinearVelocity(PIPE_VELOCITY, 0);
// up
float height = dimension.y - dnPipeHeight - CHANNEL_HEIGHT;
shape.setAsBox(dimension.x / 2, height / 2,
new Vector2(dimension.x / 2, dnPipeHeight + CHANNEL_HEIGHT + height / 2), 0);
shape.setRadius(-0.1f);
FixtureDef fixtureDefUp = new FixtureDef();
fixtureDefUp.shape = shape;
body.createFixture(fixtureDefUp);
}
public void destroy() {
for(Fixture fixture : body.getFixtureList()) {
body.destroyFixture(fixture);
}
Pipes.this.word.destroyBody(body);
}
...
}
}
首先看一下Pipe内部类,我们修改了beginToSimulate()的实现原理,之前我们是为每个Pipe维护两个Body对象,这样处理其实完全可以,但是BOX2D的Body对象具有支持多Fixtue特性,所以我们完全没有必要创建两个Body,因为我们之后还需要释放Body和Fixtue对象,所以创建一个Body对象更利于管理。接下来我们为Pipe添加了destroy()方法,我们在该方法内销毁了body的所有Fixture对象和Body对象本身。
接下来看看Pipes对象的testPipeNumberIsTooLarge(),在该方法我们内测试如果Pipe对象的数量过大,则销毁一个Pipe对象相应的Body和Fixture对象。接下来我们测试应用:
现在是不是发现游戏流畅了许多!如果你觉得Bird的下降速度太慢,难度较小可以修改一下WorldController.initWorld()方法,将world对象的重力加速度修改大一些。我这里是world = new World(new Vector2(0, -110.8f), false)这里还可以调节。本章我们创建了碰撞检测逻辑以及处理一些细节问题,下一章我们将为游戏添加分数、帧率、各种按钮等等GUI信息。
转载于:https://my.oschina.net/u/2432369/blog/610409