我们都知道Libgdx只是一个2D游戏框架,并不是游戏引擎,著名的AndEngine 2D游戏引擎就是基于Libgdx开发的,并且Flappy Bird原版就是使用AndEngine引擎开发的。所以我们必须先创建自己游戏引擎。
创建UML类图
首先我们需要分析Flappy Bird的项目架构。我们使用类图帮助我们可视化项目结构并理解项目构成。下面是该类图:
上图中FlappyBirdMain是项目主类,并且必须继承于LibGDX的ApplicationListener接口。主类包含了一个Assets类引用,Assets类被用于访问和组织游戏资源。主类还包含了WorldController和WorldRenderer两个类的句柄。
WorldController类包含了应用的初始化和所有游戏逻辑。它管理所有游戏对象并控制他们的行为。
WorldRenderer类被用于渲染每个游戏对象和需要的GUI信息。
倒数第二行的三个类继承于AbstractGameObject抽象类。他们共用一组接口,公共接口对所有游戏对象具有相同的操作功能,并能将对象渲染到场景。
下面是Flappy Bird的三个游戏对象:
Bird:玩家控制的角色
Pipe:我们看到的绿色管道。
Land:水平移动的地面。
实现Constants类
package com.art.zok.flappybird.util;
public class Constants {
// 视口宽度为20米
public static final float VIEWPORT_WIDTH = 50f;
// 视口高度为20米
public static final float VIEWPORT_HEIGHT = 50f;
}
首先我们将视口尺寸定义为50*50米,其实这里的“米”是一个相对单位。比如说我们在一个分辨率为480*800的窗口中,我们将视口设定为50*50米,则竖直方向上每米表示800/50像素,水平方向每米表示480/50像素。我们发现如果将视口设定为固定值,则水平和竖直方向上的单位长度将会不统一,放心,后面我们将会考虑到该问题并会得到有效解决。还有,这里为什么设置为50米呢?其实理论上视口尺寸可以被设定为任意值,因为他只是一个相对单位。但是因为我们后续将使用BOX2D作为碰撞检测和物理模拟的引擎,而BOX2D有一个不成文的规定,当物体的尺寸位于0.01-10米内时可以获得最大的模拟性能。
接下来我们将实现FlappyBirdMain类
package com.art.zok.flappybird;
import com.badlogic.gdx.ApplicationListener;
public class FlappyBirdMain implements ApplicationListener{
private static final String TAG =
FlappyBirdMain.class.getName();
private WorldController worldController;
private WorldRenderer worldRenderer;
@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() { }
}
FlappyBirdMain类实现了ApplicationListener接口,那么现在就成了LibGDX的主类。
WorldController引用和WorldRenferer引用使FlappyBirdMain能够更新并控制游戏的运行,并且将游戏的当前状态渲染到屏幕上。
TAG常量被设置为类名,表示一个独一无二的标签。它是为了记录日志而创建的。
实现WorldController类
package com.art.zok.flappybird.game;
public class WorldController {
private static final String TAG =
WorldController.class.getName();
public WorldController() { }
private void init() { }
public void update(float deltaTime) { }
}
该类使用一个私有的init()方法初始化内容。虽然所有的初始化代码都可以放在构造函数中,但是将初始化代码放在独立的方法中是非常有好处的,当任何时候我们想重置一个对象,而我们又不想完全重建它,或者根本没有必要完全重建,这时独立的初始化函数就节省了许多资源并提高了游戏执行效率。还有,这个方法还可以大大减少Garbage Collector(GC)引起的中断。相反,初始化函数可以让我们重新激活存在的对象,而且这也是我们推荐的方法,因为这可以在有限的资源最大化执行效率。对于资源量很小的移动设备,这点显得更重要了。
update()方法包含了游戏逻辑代码,他可能会在每秒被访问几百次。他需要一个增量时间作为参数,并且他将根据该时间增量更新游戏。
实现WorldRenderer类
package com.art.zok.flappybird.game;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.utils.Disposable;
public class WorldRenderer implements Disposable {
private OrthographicCamera camera;
private SpriteBatch batch;
private WorldController worldController;
public WorldRenderer(WorldController worldController) { }
private void init() { }
public void render() { }
public void resize(int width, int height) { }
@Override
public void dispose() { }
}
该类也包含一个初始化函数init()。其中还包含了一个render()方法,在该方法中我们定义了所有对象渲染的逻辑。当窗口尺寸发生改变时包括应用启动时窗口的改变都应该调用resize()以适应最新配置。
渲染过程使用一个正交投影相机完成二维渲染。幸运的是,LibGDX提供了一个OrthographicCamera类简化了二维渲染任务。然而,SpriteBatch类才是真正的工作者,它按照相机的设置(位置,投影,缩放等等)将所有对象渲染到屏幕上。因为SpriteBatch实现了Disposable接口,因此当SpriteBatch实例不再需要的时候应当呼叫dispose()方法释放其占用的内存。为了达到这一目的,我们也为WorldRenerer类实现了Disposable接口,并在dispose方法中调用SpriteBatch的dispose方法,这使得当FlappyBirdMain类的dispose()方法被调用后可以很容易的执行有序释放(清理)过程。
需要注意,该类在构造函数中需要引用一个WorldController实例以便后续渲染时可以访问控制器管理的所有对象。
组织架构
修改FlappyBirdMain类的create方法:
@Override
public void create() {
// 设定日志记录级别
Gdx.app.setLogLevel(Application.LOG_DEBUG);
worldController = new WorldController();
worldRenderer = new WorldRenderer(worldController);
}
首先,我们设置了日志记录的级别。然后创建了WorldController和WorldRenderer类实例。
接下来修改该类的render()方法:
@Override public void render() {
// 根据最后一帧到当前的增量时间跟新游戏
worldController.update(Gdx.graphics.getDeltaTime());
// 设定清屏颜色为浅蓝色
Gdx.gl.glClearColor(0x64/255.0f, 0x95/255.0f, 0xed/255.0f, 0xff/255.0f);
//清屏
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
//将游戏世界渲染到屏幕上
worldRenderer.render();
}
首先我们根据增量时间更新游戏内容。Gdx.graphics模块提供了getDeltaTime()方法获得当前的增量时间。接下来,LibGDX通过Gdx.gl模块直接调用两个OpenGL底层方法。第一个方法是glClearColor()设定清屏时使用的颜色,该方法需要使用red,green,blue和alpha(RGBA)四个值作为参数。每个通道的值需要使用一个介于0-1的浮点数表示8位二进制数的范围。第二个方法glClear()首先擦除屏幕上显示的所有内容然后使用上述设置的颜色填充整个屏幕。最后一步,将更新后的游戏世界渲染到屏幕上。
接下来修改resize()方法
@Override
public void resize(int width, int height) {
worldRenderer.resize(width, height);
}
无论任何时候窗口尺寸发生变化,ApplicationListener接口的resize()方法将被调用。因为窗口尺寸改变事件与场景渲染息息相关,因此我们将其处理代码放在WorldRenderer类中。所以这里只需要简单的调用worldRenderer.resize方法。
修改dispose()方法
@Override public void dispose() {
worldRenderer.dispose();
}
当资源不在使用时,系统将调用FlappyBirdMain.dispose()方法,该方法总是调用worldRenderer.dispose()方法释放资源。
因为Android系统还具有两个额外的系统事件,分别是pause和resume。我们希望当接收到pause或resume事件时我们的应用可以暂停和恢复。为了达到这个目的,我们需要创建一个paused成员变量。
private boolean paused;
然后修改create()和render()方法,修改完成之后如下:
@Override
public void create() {
// 将LibGDX的日志级别设定为DEBUG
Gdx.app.setLogLevel(Application.LOG_DEBUG);
// 初始化控制器和渲染器
worldController = new WorldController();
worldRenderer = new WorldRenderer(worldController);
// 启动时激活游戏
paused = false;
}
@Override
public void render() {
// 当游戏暂停时不更新游戏世界
if(!paused) {
// 根据最后一帧到当前帧的增量时间更新游戏世界
worldController.update(Gdx.graphics.getDeltaTime());
}
// 设置清屏颜色为:浅蓝色
Gdx.gl.glClearColor(0x64/255.0f, 0x95/255.0f, 0xed/255.0f, 0xff/255.0f);
// 清屏
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
// 将游戏世界渲染到屏幕上
worldRenderer.render();
}
最后响应暂停和恢复事件,在pause()和resume()方法添加相应的代码:
@Override
public void pause() {
paused = true;
}
@Override
public void resume() {
paused = false;
}
到现在为止我们的框架就已经建立完毕了,现在就可以尝试运行了。
本章内容到现在就已经全部介绍完了,本章我们分析并创建了游戏的框架。下一章我们将介绍资源的打包方法。
转载于:https://my.oschina.net/u/2432369/blog/610414