LibGDX教程——重建Flappy Bird——(5) 添加Box2D物理仿真和游戏逻辑
来源:互联网 发布:网络诈骗50万100万 编辑:程序博客网 时间:2024/06/05 03:37
本章源码链接:http://pan.baidu.com/s/1o6Tt6VS密码:dvsc
首先,我们为三个对象添加一个共同的方法,既然是共同的方法,那么我们就应该添加到三者公用的父类中,然后再子类中重写该方法。
修改AbstractGameObject:
package com.art.zok.flappybird.game.object;import com.badlogic.gdx.graphics.g2d.SpriteBatch;import com.badlogic.gdx.math.MathUtils;import com.badlogic.gdx.math.Vector2;import com.badlogic.gdx.physics.box2d.Body;import com.badlogic.gdx.physics.box2d.World;public abstract class AbstractGameObject {public Vector2 position;public Vector2 dimension;public Vector2 origin;public Vector2 scale;public float rotation;public Body body;public AbstractGameObject() {position = new Vector2();dimension = new Vector2(1, 1);origin = new Vector2();scale = new Vector2(1, 1);rotation = 0;}public void beginToSimulate(World world) {}public void update(float deltaTime) {if(body != null) {position.set(body.getPosition());rotation = body.getAngle() * MathUtils.radiansToDegrees;}}public abstract void render(SpriteBatch batch);}
为对象创建Body
首先,我们为Bird对象重写beginToSimulate()方法:
@Overridepublic void beginToSimulate(World world) {BodyDef bodyDef = new BodyDef();bodyDef.type = BodyType.DynamicBody; // 必须是动态对象bodyDef.fixedRotation = true; // 固定角度bodyDef.position.set(position);body = world.createBody(bodyDef);body.setUserData(this);PolygonShape shape = new PolygonShape();shape.setAsBox(dimension.x / 2, dimension.y / 2);shape.setRadius(-0.4f);FixtureDef fixtureDef = new FixtureDef();fixtureDef.density = 0.1f;fixtureDef.friction = 0f;fixtureDef.shape = shape;body.createFixture(fixtureDef);}
@Overridepublic void beginToSimulate(World world) {BodyDef bodyDef = new BodyDef();bodyDef.type = BodyType.KinematicBody;// 运动物体bodyDef.position.set(position);// 初始位置body = world.createBody(bodyDef);body.setUserData(this);PolygonShape shape = new PolygonShape();shape.setAsBox(dimension.x * 1.5f, dimension.y / 2, new Vector2(dimension.x * 1.5f, dimension.y / 2), 0);// 矩形边界shape.setRadius(-0.1f);FixtureDef fixtureDef = new FixtureDef();fixtureDef.shape = shape;body.createFixture(fixtureDef);body.setLinearVelocity(LAND_VELOCITY, 0);}Land对象的Body和Bird对象的Body存在两点区别,第一Land对象的Body属于KinematicBody物体,所以在fixtureDef我们没有定义它的密度和摩擦力等等;第二我们使用setLinearVelocity为Body设定了一个水平向左的速度。
为Pipe类重写beginToSimulate()方法:
@Overridepublic void beginToSimulate(World world) {// downBodyDef bodyDef = new BodyDef();bodyDef.type = BodyType.KinematicBody;bodyDef.position.set(position);Body b = 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;b.createFixture(fixtureDefDown);b.setLinearVelocity(PIPE_VELOCITY, 0);body = b;// upbodyDef.position.set(position.x, position.y + dnPipeHeight + CHANNEL_HEIGHT);b = world.createBody(bodyDef);shape.setAsBox(dimension.x / 2, (dimension.y - dnPipeHeight - CHANNEL_HEIGHT) / 2, new Vector2(dimension.x / 2, (dimension.y - dnPipeHeight - CHANNEL_HEIGHT) / 2), 0);shape.setRadius(-0.1f);FixtureDef fixtureDefUp = new FixtureDef();fixtureDefUp.shape = shape;b.setLinearVelocity(PIPE_VELOCITY, 0);b.createFixture(fixtureDefUp);}同样Pipe类也是KinematicBody类型物体,不同的是,我们这里创建了两个body,这是因为我们在同一Pipe对象中绑定了上下两个游戏中的管子或者说是柱子。然而,成员变量body中保存的是下面(down)那个管子的Body对象,这样做是因为我们的Pipe对象的位置和该Body对象的位置相同,所以我们总是以下面(down)这个Body对象为标准。
...public class Pipes extends AbstractGameObject { ....private World word;public Pipes(World world) { ...init(world);}public void init(World world) {this.word = world; ...}...public class Pipe extends AbstractGameObject { ...public void init() {...beginToSimulate(Pipes.this.word);} ...}}上面我们为Pipes维护了一个World类型成员变量world,我们发现,我们是在Pipes.init()方法中初始化world变量而不是在构造方法中,这样做是因为每次调用init()方法重新开始游戏时我们的WorldController类都会重建World对象。
使用BOX2D仿真
开始使用BOX2D仿真之前我们还需要准备一下,首先为WorldController维护一个world对象并添加一个isStart成员变量:
...public class WorldController extends InputAdapter implements Disposable { private static final String TAG = WorldController.class.getName(); World world; boolean isStart; ... private void init() { isStart = false; initWorld(); ... Gdx.input.setInputProcessor(this); } private void initWorld() { if(world != null) world.dispose(); world = new World(new Vector2(0, -9.8f), false); } ... public void update(float deltaTime) { // 如果开始游戏则开始模拟 if(isStart) { world.step(deltaTime, 8, 3); } bird.update(deltaTime); ... } @Override public boolean touchDown(int screenX, int screenY, int pointer, int button) { if(!isStart) { isStart = true; bird.beginToSimulate(world); land.beginToSimulate(world); //pipes.beginToSimulate(world); } return super.touchDown(screenX, screenY, pointer, button); } @Override public void dispose() { world.dispose(); }}
首先我们为WorldController添加了一个world成员和一个isStart成员变量,分别表示Box2D的世界和游戏是否开始。接下来我们在初始化方法init()中将isStart初始化为false并调用了initWorld()方法初始化world,在initWorld()中如果world成员变量不为空则销毁他并重新创建新的World对象,World类的构造函数需要两个参数,第一个表示x和y两个方向上的重力加速度,因为在Libgdx中y轴正方向是竖直向上的,所以这里的重力设置为-9.8m/s²。
在update()方法中,我们首先判断isStart是否等于true,如果为true则表示游戏已经开始,我们必须开始进行BOX2D模拟。BOX2D要进行物理仿真则必须在每个更新循环调用World.step()方法,该方法第一个参数是步长,也就是上一帧开始渲染到该帧开始渲染的间隔时间,也称为增量时间,所以直接将deltaTime传入即可,接下来两个整型参数分别表示速度迭代次数和位置迭代次数,理论上是越高越精确,但是设置太高会严重影响物理仿真的性能,所以需要酌情设置。
我们还为WorldController实现了Disposable接口并重写dispose()方法,并在该方法执行了world.dispose()释放内存。既然WorldController实现了Disposable接口,就说明WorldController在不需要的时候需要释放相应的内存资源,事实也正是这样,所以我们也必须在拥有WorldController实例的FlappyBirdMain类的dispose()方法中调用其dispose()方法。
@Override public void dispose() { worldRenderer.dispose(); worldController.dispose(); }还有,我们让WorldController继承了InputAdapter类,该类是LIBGDX的输入事件适配器。只要我们想响应键盘和鼠标的输入或者触摸屏的触摸,我们只需要实现并创建一个InputAdapter实例,然后将其作为参数调用Gdx.input.setInputProcessor()方法,这样就相当告诉LibGDX,我们想让当前所有输入事件由该类实例进行响应,然后我们只需要重写相应的方法便可响应输入事件。这里我们重写了touchDown方法,该方法既可以测试鼠标的点击事件也可以测试触摸屏的触摸事件,前两个参数表示点击或者触摸事件的屏幕位置(单位是像素)并且坐标系是以左上角为坐标原点的;如果是屏幕触摸则第三个参数表示是触摸的第几个点,这里也表明LibGDX是支持多点触控的;第四个参数等于Buttons类的常量之一,如果是触摸事件第四个参数始终等于Buttons.LEFT;该方法的返回值表示是否结束touchDown事件,如果返回true则LibGDX认为已经完成该事件的处理,则不会在继续调用下一个输入适配器的同一个方法。在该方法中,如果isStart等于false,我们首先将为isStart赋值为true表明开始物理仿真,即游戏开始。接下来我们分别为Bird对象和Land对象调用beginToSimulate()方法创建相应的Body。
经过上述分析,我们添加的代码一开始处于未开始状态,当鼠标点击窗口或者手指触摸屏幕则开始游戏。下面测试并应用:
private void wrapLand() {if (leftMoveDist >= viewWidth) {position.x = -viewWidth / 2 + leftMoveDist - viewWidth;leftMoveDist = 0;if(body != null) {body.setTransform(position, 0);}}}我们引入一个新方法Body.setTransform()。BOX2D允许kinematicBody类型物体使用该方法进行2D空间变换,该方法的第一个参数是新位置的坐标,第二个参数是旋转的角度,这里我们分别设置为position和0。
接下来我们可以重新测试:
现在可以观察到各个对象和背景都能正常工作了,但是如果当你切换到其他窗口等待几秒钟之后在返回到该应用窗口,就会发现另有问题:
上面这个问题是因为当系统切换到其他窗口后,本应用将进入暂停状态,当重新回到应用后又恢复执行,但是恢复后执行第一帧渲染时的增量时间deltaTime由于重新加载资源导致事件变得特别长(大概是0.19秒左右)造成的。解决该问题我们只需要稍微修改一下FlappyBirdMain的render方法即可:
@Override public void render() { if(!paused) { // 增量时间不能大于0.018秒 float deltaTime = Math.min(Gdx.graphics.getDeltaTime(), 0.018f); worldController.update(deltaTime); } ... }BOX2D的调试渲染
...public class WorldRenderer implements Disposable { ... private Box2DDebugRenderer debugRenderer; ... private void init() { batch = new SpriteBatch(); debugRenderer = new Box2DDebugRenderer(); ... } public void render() { //renderWorld(batch); debugRenderer.render(worldController.world, camera.projection); } ...}
debugRenderer需要两个参数,第一个是需要渲染的BOX2D世界对象world,第二个是视口的投影矩阵。由于BOX2D的调试渲染只绘制了Body对象的轮廓,所以为了避免游戏对象渲染的纹理干扰调试渲染,所以我们屏蔽renderWorld(batch)方法的调用。为了更清晰可见,我们应该将清屏颜色设置为黑色(0,0,0,1)。完成上述修改后重新运行:
观察截图可以发现Bird对象的位置和Land、Pipe对象均有相交,这就是创建Body时调用shape.setRadius()的结果。调试完成需要将上述添加代码注释掉,如果觉得麻烦你也可以创建一个boolean类型的常量进行条件判断是否调试渲染,这样修改起来则方便许多。
创建Bird对象跳跃逻辑
首先Bird跳跃有两种运动过程,第一是上下快速平移;第二是绕着中心点旋转。因为我们使用的是BOX2D模拟Bird的运动过程,因此第一种运动过程很容易实现:我们只需要在每次点击时为Bird附加一个竖直向上的速度,这时,因为有重力加速度的作用,Bird对象会开始经历一个先减速上升而后加速下降的过程,这正符合我们的预期,至于仿真度的高度那就参数大小问题了。我们仔细观察原游戏的Bird运动过程,我们就可以发现Bird的旋转过程是跟速度有关系的,当速度小于某个值时Bird以较慢的速度顺时针旋转,速度大于某个值时Bird以较快的速度逆时针旋转,并且Bird的旋转角度具有确定的范围,所以我们可以设定一个速度阀值,然后个根据Bird当前的速度分成成两种情况对Bird对象的rotation变量进行持续更新,必须强调一点,更新Bird对象的旋转角度时,我们也必须更新Body对象,否则两者的边界矩形将不能重合,这样碰撞模拟将显得非常不真实。
根据上述分析,我们现在来改造Bird对象,首先我们实现第一种运动状态,为Bird对象添加一个新方法setJumping():
// 触发跳跃public void setJumping() {if(body != null) {body.setLinearVelocity(0, 35f);}}
public class Bird extends AbstractGameObject {protected static final float BIRD_MAX_FLAP_ANGLE = 20; // 逆时针旋转最大角度protected static final float BIRD_MAX_DROP_ANGLE = -90; // 顺时针旋转最大角度protected static final float FLAP_ANGLE_DRAG = 9.0f; // 逆时针旋转速度protected static final float BIRD_FLAP_ANGLE_POWER = 6.0f; // 顺时针旋转速度 protected static final float SPEED_THRESHOLD = -20f;// 速度阀值 ...}上面每个厂里都给出了解释,下面修改Bird.update()方法:
@Overridepublic void update(float deltaTime) {if (body == null) {if (waveState == WAVE_STATE.WAVE_FALLING)position.y -= 0.05f;else if (waveState == WAVE_STATE.WAVE_RISE) {position.y += 0.05f;}if (position.y < min_wave_height) {waveState = WAVE_STATE.WAVE_RISE;} else if (position.y > max_wave_height) {waveState = WAVE_STATE.WAVE_FALLING;}} else {super.update(deltaTime);// 根据速度计算最新旋转角度if (body.getLinearVelocity().y < SPEED_THRESHOLD) {rotation -= BIRD_FLAP_ANGLE_POWER;} else {rotation += FLAP_ANGLE_DRAG;}// 限制旋轉角度在20到-90度之間rotation = MathUtils.clamp(rotation, BIRD_MAX_DROP_ANGLE, BIRD_MAX_FLAP_ANGLE);body.setTransform(position, rotation * MathUtils.degreesToRadians);}}在update()方法中,当body等于null则属于前面我们介绍的游戏未开始时状态,所以不用多说。当body不等null则表明游戏已经开始,我们首先调用父类update()方法,然后通过body.getLinearVelocity()方法获得body的速度矢量,接着通过竖直方向的速度和速度阈值作比较我们分成了两种情况处理,即顺时针和逆时针。在LibGDX中,逆时针旋转为正,顺时针旋转为负,所以在顺时针的情况我们为rotation每帧减去BIRD_FLAP_ANGLE_POWER度,逆时针我们为rotation每帧增加FLAP_ANGLE_DRAG度。接着我们调用MathUtils.clamp()方法将rotation现在[-90,20]度之间。最后,我们变换body对象的选择角度,因为这里我们只是变换角度,所以位置position并没有发生改变,还有,在BOX2D中角度都是以弧度表示的,所以我们需要将rotation转换为弧度,LibGDX提供了一个方便的常量MathUtils.degreesToRadians可以将角度转换为弧度。
@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);}bird.setJumping();}return true; }到现在为止,Bird已经可以完成上面要求的行为逻辑了。我们可以启动测试一下:
- LibGDX教程——重建Flappy Bird——(5) 添加Box2D物理仿真和游戏逻辑
- LibGDX教程——重建Flappy Bird——(2) 创建游戏框架
- LibGDX教程——重建Flappy Bird——(4) 创建游戏对象
- LibGDX教程——重建Flappy Bird——(7) 添加GUI信息
- Libgdx教程——重建Flappy Bird——(1) 项目创建与导入
- LibGDX教程——重建Flappy Bird——(3) 打包资源
- LibGDX教程——重建Flappy Bird——(6) 碰撞检测及细节处理
- LibGDX教程——重建Flappy Bird——(8)屏幕切换与播放声音(终结)
- 游戏——Flappy Bird
- 【Phaser游戏列表】——Flappy-bird
- unity视频笔记——flappy bird
- BZOJ4723——[POI2017]Flappy Bird
- win下最后一个游戏!——flappy bird(beta) from cocos2d-x
- 用Phaser来制作一个html5游戏——flappy bird (一)
- 用Phaser来制作一个html5游戏——flappy bird (二)
- android游戏开发框架libgdx的使用(九)—在libgdx中使用Box2d
- android游戏开发框架libgdx的使用(九)—在libgdx中使用Box2d
- Ubuntu Cocos2dx 学习笔记——添加Box2d物理游戏引擎
- 史上最全AndroidStudio快捷键中文版
- makefile中的CFLAGS与LDFLAGS
- 2015CSDN下载热门书籍
- JS原型链 new 与 Object.Create()区别 代码及继承的方法
- Back Track 5 之 漏洞攻击 && 密码攻击 && Windows下渗透工具
- LibGDX教程——重建Flappy Bird——(5) 添加Box2D物理仿真和游戏逻辑
- Java学习第8天(4):面向对象-继承-abstract
- 竟然有92%的年轻创业者认为事业比家庭重要,三分之一的人创业成功后想移民!
- ViewPager 的使用
- AJAX学习笔记(五)——JSON格式
- Spring Integration实例代码分析之basic--http
- mongodb
- SQL Server中的事务与锁
- QT5.5 webengine 打开browser 后调用 web 的 JavaScript