Libgdx 是一个用java开发的一款跨平台游戏框架。它目前支持的平台有 Windows,Mac OS X,Android,IOS 和 HTML5。下面使用LibGDX实现一个水桶接水滴的一个小游戏。
项目设置
这里使用 LibGDX提供的项目创建工具LibGdx Project Setup Tool来创建一个游戏项目,该工具的获取可以查看置顶评论,设置如下:
- Application name: drop
- Package name: com.badlogic.drop
- Game class: Drop
- 其他设置不动就可以了

工具默认选择Desktop和Android,这里可以只选用Desktop来做测试,如果需要在安卓上运行,也可以勾选上Android, 但是勾选上后需要配置好安卓的开发环境,其他平台也是同理。
工具生成的项目其实就是一个gradle项目工程,可以选择自己喜欢的IDE来进行导入,这里我将其导入到Eclipse IDE中。在drop-desktop模块下点击DesktopLauncher运行,效果如下:

游戏设计
该游戏的规则很简单:
- 用一个水桶去接雨滴。
- 水桶位于屏幕的底部。
- 雨滴每秒在屏幕顶部随机产生,并向下加速。
- 玩家可以通过鼠标水平移动、触摸或者移动左右键来操控水桶。
游戏资源
在这个游戏中需要的资源有如下几个:
- 滴水的音效声
- 下雨的声音
- 雨滴资源图片
- 水桶资源图片
资源也在评论区置顶中查看哦。
下载的资源文件中有: drop.wav、 rain.mp3、 droplet.png 和 bucket.png,将它们放在 drop_desktop/assets/中。如果有其他模块,比如android等,我们只需要随便放在其中一个assets就可以了,这里的资源是各个模块中公用的。

配置启动类
我们配置启动类。在 drop_desktop模块中打开 DesktopLauncher.java 启动类。我们设置游戏窗口的大小为800x480,并将标题设置为“ Drop”。代码如下:
package com.badlogic.drop.desktop;
import com.badlogic.gdx.backends.lwjgl.LwjglApplication;
import com.badlogic.gdx.backends.lwjgl.LwjglApplicationConfiguration;
import com.badlogic.drop.Drop;
public class DesktopLauncher {
public static void main (String[] arg) {
LwjglApplicationConfiguration config = new LwjglApplicationConfiguration();
config.title = "Drop";
config.width = 800;
config.height = 480;
new LwjglApplication(new Drop(), config);
}
}
游戏核心逻辑
我们的游戏核心逻辑都是在drop_core模块中,我们打开drop_core模块下的Drop.java。

删除其默认生成的代码,只留下以下的基础代码:
public class Drop extends ApplicationAdapter {
@Override
public void create() {
}
@Override
public void render () {
}
}
加载资源
通常在其create方法中加载游戏相关的资源。代码如下:
public class Drop extends ApplicationAdapter {
private Texture dropImage;
private Texture bucketImage;
private Sound dropSound;
private Music rainMusic;
@Override
public void create() {
// 加载雨滴和桶的资源图片,每个像素大小为64*64
dropImage = new Texture(Gdx.files.internal("droplet.png"));
bucketImage = new Texture(Gdx.files.internal("bucket.png"));
// 加载雨滴音效和下雨的背景声音
dropSound = Gdx.audio.newSound(Gdx.files.internal("drop.wav"));
rainMusic = Gdx.audio.newMusic(Gdx.files.internal("rain.mp3"));
// 循环播放背景音乐
rainMusic.setLooping(true);
rainMusic.play();
// ... 其他代码块 ...
}
// ... 其他代码块 ...
}
在上面的示例代码中,每一个资源,我们在 Drop 类中都有一个字段与其对应,create ()方法是ApplicationAdapter的生命周期方法,我们通常在其create()方法中加载资源,前两行加载雨滴和桶的图像资源。Texture为纹理。通常不能直接绘制纹理。可以通过将 FileHandle 传递给资源文件的构造函数来加载纹理。这样的 FileHandle 实例通过 Gdx.files 提供的方法之一获得。在libGDX中有着不同类型的文件,我们在这里使用“Gdx.files.internal”文件类型来加载资源。Gdx.files.internal为从其资源目录assets中加载资源,所传为相对于assets的文件路径。
接下来加载雨滴音效和背景音乐。libGDX 区分了存储在内存中的声音效果和存储在任何地方的流媒体音乐。音乐通常太大而不能完全保存在内存中,因此才有了差异。一般按照经验,如果小于10秒的音频文件,则应该使用 Sound 实例,如果较长的音频片段,则应该使用 Music 实例。
注意: libGDX 支持 MP3、 OGG 和 WAV 文件。你可以根据自身需求来进行选择,每种格式都有自己的优点和缺点。例如,与其他格式相比,WAV 文件相当大,OGG 文件在 RoboVM (iOS)和 Safari (GWT)上都不能工作,而且 mp3文件存在不适当的循环问题。
示例代码中通过 Gdx.audio.newSound ()和 Gdx.audio.newMusic ()加载音效资源。这两个方法都采用 FileHandle。
在 create ()方法的最后,我们还设置了 Music 实例为立即循环播放。运行这个应用程序,你会听到哗啦哗啦的下雨声。
Camera和SpriteBatch
接下来,我们在Drop类中创建一个相机Camera和一个 SpriteBatch。SpriteBatch 是一个特殊的类,用于绘制二维图像,比如将纹理绘制到屏幕上。
private OrthographicCamera camera;
private SpriteBatch batch;
然后在create() 方法中实例化它:
camera = new OrthographicCamera();
camera.setToOrtho(false, 800, 480);
batch = new SpriteBatch();
这里创建了一个区域为800*400单位的相机。把它想象成一个通向游戏世界的虚拟窗口。目前,我们可以将这些单位理解为像素。但是将其理解为其他的单位也没错,比如米或者其他你想要的单位。相机是非常强大的,允许你做很多事情。
SpriteBatch在下面具体细说。
添加水桶
接下来往我们的游戏中添加水桶。为了描述水桶和雨滴,我们需要存储它们的位置和大小。libGDX 提供了一个 Rectangle 类,我们可以为此使用它。让我们从创建一个矩形代表我们的桶开始。我们给Drop类添加了一个新字段:
private Rectangle bucket;
在 create ()方法中,我们实例化了 Rectangle 并指定了它的初始值。我们希望桶在屏幕底部边缘上方20像素,水平居中。
bucket = new Rectangle();
bucket.x = 800 / 2 - 64 / 2;
bucket.y = 20;
bucket.width = 64;
bucket.height = 64;
我们将桶水平放置在屏幕底部边缘上方20像素的位置。默认情况下,libGDX (和 OpenGL)中的所有展现都是在 y轴朝上的情况下执行的。桶的 x/y 坐标定义了桶的左下角,绘图原点位于屏幕的左下角。矩形的宽度和高度被设置为64x64。
注意: 这种设置是可以更改的,使 y 轴向下,原点在屏幕的左上角。OpenGL 和相机类是很灵活的,你可以使用几乎任何你想要的视角,在 2D 和 3D中。然而,一般不推荐这样干。
渲染水桶
渲染方法为ApplicationAdapter的生命周期render方法。第一步我们先用深蓝色清除屏幕:
ScreenUtils.clear(0, 0, 0.2f, 1);
ScreentUtils.clear的参数为red, green, blue 和 alpha, 每一个分量为[0-1]之间。
接下来,我们需要调用相机的update方法更新相机。相机使用矩阵的数学实体,负责建立渲染坐标系。每次我们更改了相机的任何属性,比如它的位置,相机的这些矩阵都需要重新update更新计算。在此示例中我们虽然还没有涉及到相机的相关操作,但是通常每帧更新一次相机是一个很好的习惯:
camera.update();
现在我们可以渲染我们的水桶了:
batch.setProjectionMatrix(camera.combined);
batch.begin();
batch.draw(bucketImage, bucket.x, bucket.y);
batch.end();
第一行告诉 SpriteBatch 使用相机指定的坐标系。如前所述,这是通过数学矩阵来完成的,更具体地说,就是一个投影矩阵。camera.combined字段就是这样的一个矩阵。从那里,在 SpriteBatch 上将呈现前面描述的坐标系中的所有内容。
接下来,我们告诉 SpriteBatch 准备开始绘制。我们为什么要用batch绘制呢?因为OpenGL最讨厌提供给他渲染的是一个一个的单张图片,OpenGL希望是一次性提交给它尽可能多的渲染的图片。所以这里使用batch,批次的提交给OpenGL渲染。
SpriteBatch 类是一个对 OpenGL 很友好的类。它将记录在 SpriteBatch.begin ()和 SpriteBatch.end ()之间的所有绘图命令。一旦我们调用了 SpriteBatch.end () ,它将同时提交所有的绘图请求,这将大大加快渲染速度。这些在一开始看起来可能有些麻烦,但是这将提高渲染速度,在60帧率渲染500个 sprites 和在20帧率渲染100个 sprites 之间是有很大区别的。
运行目前我们的示例代码,我们可以看到如下的效果:

通过鼠标/触摸使桶移动
现在我们让用户来控制桶。之前我们说过我们会允许用户拖动这个桶。我们可以先做一个简化一点的功能。如果用户触摸屏幕(或按下鼠标按钮),我们希望桶水平围绕该位置居中。在render的最后添加如下代码:
if(Gdx.input.isTouched()) {
Vector3 touchPos = new Vector3();
touchPos.set(Gdx.input.getX(), Gdx.input.getY(), 0);
camera.unproject(touchPos);
bucket.x = touchPos.x - 64 / 2;
}
首先,我们通过调用 Gdx.input.isTouched ()来询问输入模块屏幕当前是否被触摸(或鼠标按钮是否被按下)。接下来,我们要将触摸/鼠标的坐标转换为相机的坐标系。这是必要的,因为我们得到的触摸/鼠标坐标的坐标系可能不同于我们用来表示游戏世界中物体的坐标系。
Gdx.input.getX()和Gdx.input.getY()返回当前的触摸/鼠标位置(libGDX 也支持多点触摸,这在后续的文章中详细介绍)。为了将这些坐标转换为我们相机的坐标系,我们需要调用 camera.unproject ()方法,它请求一个 Vector3,一个三维矢量。我们创建这样一个向量,设置当前的触摸/鼠标坐标并调用该方法。该三维矢量现在将包含我们的水桶所在的坐标系中的触摸/鼠标坐标。最后,我们将水桶的位置更改为围绕触摸/鼠标坐标的中心。
注意1: 在这里的代码中,实例化大量新对象是非常非常糟糕的,例如 vector3实例。这是因为垃圾收集器必须频繁地收集这些短命的对象。虽然在桌面上这没什么大不了的(由于可用的资源) ,但在 Android 上 GC 可能会导致长达几百毫秒的暂停,从而导致卡顿。在这种特殊情况下,如果您想解决这个问题,只需将 touchPos 作为 Drop 类的私有最终字段,而不是在render方法中一直实例化它。
注意2: touchPos 是一个三维向量。OrthographicCamera 实际上是一个3d 相机,它也考虑了 z 坐标。想想 CAD 的应用,他们也使用三维正投影相机。我们只是简单地用它来绘制二维图形。
现在我们就得到了如下效果,当我们失败点击其中某个点的时候,水桶就会在底部跑到我们点击的那个位置的下方。

通过键盘使桶移动
在桌面和浏览器上,我们也可以接收键盘输入。当按键左或按键右被按下时,让桶发生移动。
我们希望桶在按键按下并且不需要加速的情况下,以每秒200个像素/单位的速度向左或向右移动。为了实现这种基于时间的移动,我们需要知道从最后一个渲染帧到当前渲染帧之间的时间。我们可以这样做:
if(Gdx.input.isKeyPressed(Input.Keys.LEFT)) {
bucket.x -= 200 * Gdx.graphics.getDeltaTime();
}
if(Gdx.input.isKeyPressed(Input.Keys.RIGHT)) {
bucket.x += 200 * Gdx.graphics.getDeltaTime();
}
方法 Gdx.input.isKeyPressed ()告诉我们是否按下了特定的键。Keys 枚举包含 libGDX 支持的所有按键代码。方法
gdx.graphics.getdeltatiana ()以秒为单位返回上一帧和当前帧之间传递的时间。我们所需要做的就是通过加减200个单位乘以以秒为单位的增量时间来修改桶的 x 坐标。
我们还需要限制确保我们的水桶不能跑出屏幕外面去:
if(bucket.x < 0 bucket.x='0;' ifbucket.x> 800 - 64) {
bucket.x = 800 - 64;
}
添加下雨雨滴
对于雨滴,我们在Drop类中添加一个矩形实例的列表字段,每个实例都记录了雨滴的位置和大小:
private Array raindrops;
Array 类是一个 libGDX 提供的工具类,用于替代 ArrayList 等标准 Java 集合。后者的问题在于它们以各种方式产生垃圾。Array 类试图尽可能减少垃圾。libGDX 还提供了其他垃圾收集器感知的集合,比如散列映射或集合。
我们还需要记录上次产生雨滴的时间,所以我们添加了另一个字段:
private long lastDropTime;
我们将采用纳秒为单位的时间,所以类型这里我们选择为long。
我们新建一个spawnRaindrop ()的方法来创建雨滴,实例化一个新的 Rectangle,将其设置为屏幕顶部边缘的一个随机位置,并将其添加到雨滴数组中。
private void spawnRaindrop() {
Rectangle raindrop = new Rectangle();
raindrop.x = MathUtils.random(0, 800-64);
raindrop.y = 480;
raindrop.width = 64;
raindrop.height = 64;
raindrops.add(raindrop);
lastDropTime = TimeUtils.nanoTime();
}
这个方法显而易见。MathUtils 类是一个 libGDX 类,提供了各种与数学相关的静态方法。在这里,它将返回一个0到800-64之间的随机值。TimeUtils 是另一个 libGDX 类,它提供了一些非常基本的与时间相关的静态方法。在这里,我们以纳秒为单位记录当前时间。
在 create ()方法中,我们实例化raindrops并生成第一个雨滴:
raindrops = new Array();
spawnRaindrop();
接下来,我们在 render ()方法中添加几行代码,用于检查自产生新雨滴以来已经过去了多少时间,如果有必要,还可以创建一个新雨滴:
if(TimeUtils.nanoTime() - lastDropTime > 1000000000) {
spawnRaindrop();
}
我们还需要让雨滴移动,简单点,我们让它们以每秒200像素/单位的恒定速度移动。如果雨滴在屏幕的底部边缘之下,我们将其从数组中移除。
for (Iterator iter = raindrops.iterator(); iter.hasNext(); ) {
Rectangle raindrop = iter.next();
raindrop.y -= 200 * Gdx.graphics.getDeltaTime();
if(raindrop.y + 64 < 0) iter.remove();
}
接着我们在batch.begin()和batch.end()之间来渲染雨滴,代码如下:
for(Rectangle raindrop: raindrops) {
batch.draw(dropImage, raindrop.x, raindrop.y);
}
最后,如果我们用桶接住了雨滴,也就是一个雨滴打在桶上,我们希望回放声音并从数组中移除雨滴。我们只需在雨滴更新循环中加入以下几行:
if(raindrop.overlaps(bucket)) {
dropSound.play();
iter.remove();
}
Rectangle.overlaps()方法检查该矩形是否与另一个矩形重叠。
清理
用户可以随时关闭应用程序。对于这个简单的例子,没有什么需要做的。不过,帮助操作系统解决一点问题并清理我们造成的混乱通常是一个好习惯。
在libGDX中任何实现 Disposable 接口并因此具有 dispose ()方法的 libGDX 类在不再使用后都需要手动清理。在我们的示例中,对于纹理、声音、音乐和 SpriteBatch 来说,都是需要清理掉的。我们将在ApplicationAdapter的生命周期dispose ()方法下进行清理:
@Override
public void dispose() {
dropImage.dispose();
bucketImage.dispose();
dropSound.dispose();
rainMusic.dispose();
batch.dispose();
}
一旦您清理释放了一个资源,就不应该以任何方式访问它。
一次性资源通常是本机资源,不由 Java 垃圾收集器处理。这就是为什么我们需要手动处理它们的原因。libGDX 提供了各种帮助资产管理的方法。这在后续的章节中详细介绍。
处理暂停/恢复
安卓系统有这样一个操作: 每当用户接到一个电话或者按下 home 键时,就暂停并恢复你的应用程序。在这种情况下,libGDX 会自动为您做很多事情,例如重新加载可能丢失的图像(OpenGL 上下文丢失,这本身就是一个可怕的主题) ,暂停并恢复音乐流等等。
在我们的游戏中,没有真正需要处理暂停/恢复。只要用户返回到应用程序,游戏就会继续。通常人们会实现一个暂停屏幕,并要求用户触摸屏幕继续。可以使用 ApplicationAdapter的生命周期pause ()和 resume ()方法。
效果
在桌面上的运行效果如下:

将其运行到Android上效果为:

完整的代码
import java.util.Iterator;
import com.badlogic.gdx.ApplicationAdapter;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Input.Keys;
import com.badlogic.gdx.audio.Music;
import com.badlogic.gdx.audio.Sound;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.math.Rectangle;
import com.badlogic.gdx.math.Vector3;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.ScreenUtils;
import com.badlogic.gdx.utils.TimeUtils;
public class Drop extends ApplicationAdapter {
private Texture dropImage;
private Texture bucketImage;
private Sound dropSound;
private Music rainMusic;
private SpriteBatch batch;
private OrthographicCamera camera;
private Rectangle bucket;
private Array raindrops;
private long lastDropTime;
@Override
public void create() {
// load the images for the droplet and the bucket, 64x64 pixels each
dropImage = new Texture(Gdx.files.internal("droplet.png"));
bucketImage = new Texture(Gdx.files.internal("bucket.png"));
// load the drop sound effect and the rain background "music"
dropSound = Gdx.audio.newSound(Gdx.files.internal("drop.wav"));
rainMusic = Gdx.audio.newMusic(Gdx.files.internal("rain.mp3"));
// start the playback of the background music immediately
rainMusic.setLooping(true);
rainMusic.play();
// create the camera and the SpriteBatch
camera = new OrthographicCamera();
camera.setToOrtho(false, 800, 480);
batch = new SpriteBatch();
// create a Rectangle to logically represent the bucket
bucket = new Rectangle();
bucket.x = 800 / 2 - 64 / 2; // center the bucket horizontally
bucket.y = 20; // bottom left corner of the bucket is 20 pixels above the bottom screen edge
bucket.width = 64;
bucket.height = 64;
// create the raindrops array and spawn the first raindrop
raindrops = new Array();
spawnRaindrop();
}
private void spawnRaindrop() {
Rectangle raindrop = new Rectangle();
raindrop.x = MathUtils.random(0, 800-64);
raindrop.y = 480;
raindrop.width = 64;
raindrop.height = 64;
raindrops.add(raindrop);
lastDropTime = TimeUtils.nanoTime();
}
@Override
public void render() {
// clear the screen with a dark blue color. The
// arguments to clear are the red, green
// blue and alpha component in the range [0,1]
// of the color to be used to clear the screen.
ScreenUtils.clear(0, 0, 0.2f, 1);
// tell the camera to update its matrices.
camera.update();
// tell the SpriteBatch to render in the
// coordinate system specified by the camera.
batch.setProjectionMatrix(camera.combined);
// begin a new batch and draw the bucket and
// all drops
batch.begin();
batch.draw(bucketImage, bucket.x, bucket.y);
for(Rectangle raindrop: raindrops) {
batch.draw(dropImage, raindrop.x, raindrop.y);
}
batch.end();
// process user input
if(Gdx.input.isTouched()) {
Vector3 touchPos = new Vector3();
touchPos.set(Gdx.input.getX(), Gdx.input.getY(), 0);
camera.unproject(touchPos);
bucket.x = touchPos.x - 64 / 2;
}
if(Gdx.input.isKeyPressed(Keys.LEFT)) bucket.x -= 200 * Gdx.graphics.getDeltaTime();
if(Gdx.input.isKeyPressed(Keys.RIGHT)) bucket.x += 200 * Gdx.graphics.getDeltaTime();
// make sure the bucket stays within the screen bounds
if(bucket.x < 0 bucket.x='0;' ifbucket.x> 800 - 64) bucket.x = 800 - 64;
// check if we need to create a new raindrop
if(TimeUtils.nanoTime() - lastDropTime > 1000000000) spawnRaindrop();
// move the raindrops, remove any that are beneath the bottom edge of
// the screen or that hit the bucket. In the latter case we play back
// a sound effect as well.
for (Iterator iter = raindrops.iterator(); iter.hasNext(); ) {
Rectangle raindrop = iter.next();
raindrop.y -= 200 * Gdx.graphics.getDeltaTime();
if(raindrop.y + 64 < 0) iter.remove();
if(raindrop.overlaps(bucket)) {
dropSound.play();
iter.remove();
}
}
}
@Override
public void dispose() {
// dispose of all the native resources
dropImage.dispose();
bucketImage.dispose();
dropSound.dispose();
rainMusic.dispose();
batch.dispose();
}
}
本文暂时没有评论,来添加一个吧(●'◡'●)