LibGDX教程——重建Flappy Bird——(6) 碰撞检测及细节处理

  本章源码链接:Libgdx重建FlappyBird密码:twy2
 上一章完整的介绍了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信息。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值