在潜入libgdx提供的API之前,让我们创建一个非常简单的“游戏”,这个游戏每个模块都将触及一点以让我们有种整体感觉。我们会引入一些概念,但不会太深入。
按照 Project Setup中的步骤进行。使用以下名称:
一旦导入到Eclipse中,你应该拥有4个工程: drop, drop-android, drop-desktop 和 drop-html5。
游戏的想法很简单:
我们需要一些图片和音效来使游戏看起来稍微漂亮一些。对于图形,定义目标分辨率为800x480像素(Android横屏)。如果游戏运行的设备没有该分辨率,则缩放所有东西以适应屏幕。注意:对于 高质量的游戏,你可能考虑会不同的屏幕分辨率提供不同的资源。这本身是一个大话题,这里不深入。
雨滴和水桶在垂直方向占用的屏幕应该小于十分之一,因此定义它们的大小为64x64像素。
从以下地址获取资源:
为了让游戏可以使用这些资源,必须把它们放在Android工程的assets文件夹下。把这4个文件命名为:drop.wav, rain.mp3, droplet.png 和 bucket.png,并把它们放在 drop-android/assets/ 文件夹下。桌面应用和HTML5工程都链接至该资源文件夹,因此只需要存储一次。
鉴于我们的需求,我们现在开始配置不同的启动类。先从桌面应用开始。打开drop-desktop/下的类Main.java。我们需要一个 800x480 的窗口并设置标题为"Drop"。代码如下:
package com.badlogic.drop; import com.badlogic.gdx.backends.lwjgl.LwjglApplication; import com.badlogic.gdx.backends.lwjgl.LwjglApplicationConfiguration; public class Main { public static void main(String[] args) { LwjglApplicationConfiguration cfg = new LwjglApplicationConfiguration(); cfg.title = "Drop"; cfg.width = 800; cfg.height = 480; new LwjglApplication(new Drop(), cfg); } }
转到Android工程,我们想要应用在横屏运行。因此需要修改工程根目录下的AndroidManifest.xml,如下:
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.badlogic.drop" android:versionCode="1" android:versionName="1.0" > <uses-sdk android:minSdkVersion="5" android:targetSdkVersion="15" /> <application android:icon="@drawable/ic_launcher" android:label="@string/app_name" > <activity android:name=".MainActivity" android:label="@string/app_name" android:screenOrientation="landscape" android:configChanges="keyboard|keyboardHidden|orientation"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>
配置工具已经为我们填写了正确的值,android:screenOrientation设置为"landscape"。如果要在竖屏模式运行游戏,就把该属性设置为 "portrait"。
我们希望节省电池并禁用振动器和指南针。这需要在工程的MainActivity.java中做以下修改:
package com.badlogic.drop; import android.os.Bundle; import com.badlogic.gdx.backends.android.AndroidApplication; import com.badlogic.gdx.backends.android.AndroidApplicationConfiguration; public class MainActivity extends AndroidApplication { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); AndroidApplicationConfiguration cfg = new AndroidApplicationConfiguration(); cfg.useAccelerometer = false; cfg.useCompass = false; initialize(new Drop(), cfg); } }
我们不能定义Activity的分辨率,这是由Android操作系统设置的。像之前设置的一样,无论设备分辨率是多少,我们会缩放800x480的目标分辨率到当前设备分辨率。
最后,我们要确保HTML5工程也使用一个800x480的绘制区。因此需要修改html5工程下的GwtLauncher.java文件:
package com.badlogic.drop.client; import com.badlogic.drop.Drop; public class GwtLauncher extends GwtApplication { @Override public GwtApplicationConfiguration getConfig () { GwtApplicationConfiguration cfg = new GwtApplicationConfiguration(800, 480); return cfg; } @Override public ApplicationListener getApplicationListener () { return new Drop(); } }
注意: 我们不需要为该平台指定使用的OpenGL版本,因为它只支持2.0。
现在所有的启动类都配置完成了,让我们开始实现这个有趣的游戏。
我们希望把代码分成几部分。简单起见,我们把所有东西都放在核心工程的Drop.java文件中。
第一个任务是加载资源并储存它们的引用。通常在ApplicationListener.create()方法中加载资源,因此代码如下:
public class Drop implements ApplicationListener { Texture dropImage; Texture bucketImage; Sound dropSound; Music rainMusic; @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(); ... more to come ... } // rest of class omitted for clarity
每个资源都在Drop类中拥有一个字段,因而后续我们可以引用它。create()方法的前两行加载雨滴和水桶的图片。Texture表示一个存储于视频RAM里的已加载图片。通常不直接绘制Texture。 Texture通过向其构造器传入一个资源文件的FileHandle来加载。这种FileHandle的实例是通过byGdx.files里其中一个方法来获得的。不同的文件类型有很多,我们在这里使用 "internal" 文件类型来引用资源。Internal 的文件位于Android工程的assets目录中。Eclipse中,桌面应用和HTML5工程通过链接以引用该目录。
接下来加载音效与背景音乐。Libgdx 区分音效和音乐,音效存储在内存里,音乐无论存储在哪都会被转换为流。音乐通常太大不能完全存储在内存里,因此作此区分。根据经验,如果你的示例小于10秒则要使用一个Sound实例,更长的音频就要使用Music实例。
通过Gdx.app.newSound() 和 Gdx.app.newMusic()来加载Sound 或 Music。这两个方法都需要一个FileHandle,像Texture的构造器一样。
在create()方法末尾,我们让Music实例循环并立即播放。 如果你运行这个应用,你会看到一个漂亮的粉红色背景,能听到落雨声。
接下来我们创建 一个Camera 和 SpriteBatch。我们使用前者以保证使用目标分辨率800x480像素来呈现应用,而不管实际的分辨率是多少。SpriteBatch 是一个特殊的类用来绘制2D图形,比如我们已经加载的纹理。
我们向类中加入两个新字段,命名为 camera 和 batch:
OrthographicCamera camera; SpriteBatch batch;
在 create() 方法中我们首先这样创建 camera :
camera = new OrthographicCamera(); camera.setToOrtho(false, 800, 480);
这样就可以确保camera一直为我们展示一个800x480单位宽的游戏区。想象它是一个虚拟窗口。目前我们把像素作为单位,这样简单一些。使用其它单位也没什么,如meters或其他什么。Cameras非常强大,它能作很多事,我们在此基础手册中不再详述。查看剩下的用户手册来获取更多信息。
然后创建 SpriteBatch (仍然在 create()方法中):
batch = new SpriteBatch();
通过创建这些,我们差不多已经完成所有运行游戏所需要的东西。
最后缺少的水桶和雨滴。让我们想想要用代表描述什么:
因此,为了描述水桶与雨滴,我们需要保存它们的位置和大小。Libgdx提供一个Rectangle类可以达成这个目的。开始先创建一个表示水桶的Rectangle。添加一个新字段:
Rectangle bucket;
在 create() 方法中实例化Rectangle并指定其初始值。我们想让水桶比底部高出20像素,并水平居中。
bucket = new Rectangle(); bucket.x = 800 / 2 - 64 / 2; bucket.y = 20; bucket.width = 64; bucket.height = 64;
我们将水桶水平居中,并放在离屏幕底部20像素高的地方。等等,为什么bucket.y设置为20,不应该是480 - 20吗?默认情况下,所有在libgdx(与OpenGL)中显示的东西其y轴都指向上方。水桶的x/y坐标定义在水桶的左下角,绘图的原点位于屏幕左下角。矩形的宽高设置为64x64,小于目标分辨率高度的十分之一。
是时候渲染水桶了。首先要做的是用深蓝色清屏。更改render() 方法如下:
@Override public void render() { Gdx.gl.glClearColor(0, 0, 0.2f, 1); Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT); ... more to come here ... }
如果你使用高级类如Texture 或 SpriteBatch,那关于OpenGL你只需要知道这两行。第一个调用把清屏色设置为蓝色。其参数分别是红,绿,蓝和该颜色的透明度,每个的取值范围都是[0, 1]。下一个调用命令OpenGL直接去清屏。
然后调用camera去更新。Cameras 使用一个称作矩阵的数学实体负责建立渲染的坐标系。每次更改camera属性都要重新计算这些矩阵。我们不在这个简单的例子中做这些,但每帧更新一次camera是一个很好的实践:
camera.update();
现在可以显示水桶了:
batch.setProjectionMatrix(camera.combined); batch.begin(); batch.draw(bucketImage, bucket.x, bucket.y); batch.end();
第一行告诉 SpriteBatch 使用camera指定的坐标系。如前所述,这是由一种叫做矩阵的东西完成的,确切地说,叫投影矩阵。camera.combined字段就是这样一个矩阵。SpriteBatch将从那里在坐标系中渲染前面描述过的所有东西。
下面告诉 SpriteBatch 启动一个新的batch。为什么要这么做,batch又是什么?OpenGL 最讨厌只告诉它一个单独的图片,它希望一次性告诉它尽可能多的多个图片。
SpriteBatch 类就可以帮助 OpenGL 。它会记录SpriteBatch.begin() 和 SpriteBatch.end()之间的所有绘制命令。一旦调用SpriteBatch.end(),它会一次性把提交所有的绘画请求,这让渲染过程加速很多。刚开始这些或者看起来很烦,但正是这一点造成了每秒60帧显示500个sprite和每秒20帧显示100个sprite之间的差别。
是时候让用户控制水桶了。之前我们提到过要让用户拖动水桶。让我们稍微简化一下。如果用户触摸屏幕(或按下鼠标),我们希望水桶围绕这一点水居中。在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()来查寻输入模块当前屏幕是否被触摸(或鼠标被按下)。接下来我们把触屏/鼠标人坐标转换到camera的坐标系。这很有必要,因为触屏/鼠标的坐标系很可能我我们用来显示对象的坐标系不一致。
Gdx.input.getX() 和 Gdx.input.getY() 返回当前 触摸/鼠标位置(libgdx也支持多点触控,但这是另一个话题了)。要把这些坐标转换到我们的camera的坐标系,需要调用camera.unproject() 方法,这需要一个Vector3, 一个三维矢量。创建这样的矢量,设置当前触摸/鼠标的坐标并调用该方法。该矢量就会包含水桶所在坐标系的触摸/鼠标的坐标。最后,我们更改水桶位置以围绕触摸/鼠标的坐标居中。
注意: 总是实例化新的对象是非常非常坏的一种方法,比如这里的Vector3对象。原因是垃圾回收器不得不频繁地清除这些短命的对象。在桌面应用中这不是一个大问题,但在Android里,垃圾回收器会导致几百毫秒的暂停因而会很卡。为了解决这个特殊问题,可以简单地将touchPos作为Drop类的一个字段,而不是总是实例化。
注意#2: touchPos 是一个三维矢量。你可能想知道为什么我们只操作2D时还需要它。OrthographicCamera实际上是3D camera,它也有z坐标。想想CAD应用,它们也使用3D正交camera。我们只是简单地用它来绘制2D图形。
在桌面和浏览器中,也可以接收键盘输入。当左右方向键被按下时,使水桶移动起来。
我们希望水桶移动时不振动,无论向左或向右,每秒200像素单位。要实现这种基于时间的移动,我们需要知道最近一帧和当前帧之间经过的时间。下面是相应的做法:
if(Gdx.input.isKeyPressed(Keys.LEFT)) bucket.x -= 200 * Gdx.graphics.getDeltaTime(); if(Gdx.input.isKeyPressed(Keys.RIGHT)) bucket.x += 200 * Gdx.graphics.getDeltaTime();
Gdx.input.isKeyPressed() 方法告诉我们特定的键是否被按下。枚举类Keys包含所有libgdx支持的键码。Gdx.graphics.getDeltaTime()方法返回最近上帧和当前帧之间所经历的秒数。我们只需修改水桶的x坐标,每次加上/减去 100单位。
同时还要保证水桶处于屏幕范围内。
if(bucket.x < 0) bucket.x = 0; if(bucket.x > 800 - 64) bucket.x = 800 - 64;
对雨滴来说,我们保存一个Rectangle实例列表,每一个用来跟踪一个雨滴的位置及大小。为这个列表加入一个字段:
Array<Rectangle> raindrops;
Array 类是libgdx的一个公共类用来替代标准Java集合如ArrayList。后者的问题是很多情况下它会产生垃圾。Array类尝试尽可能多地减少垃圾。Libgdx提供其他垃圾回收器可以回收的集合,如hashmaps或sets等。
我们也需要保持跟踪产生雨滴的最后时间,因此我们添加另一个字段:
long lastDropTime;
我们要用纳秒来存储这个时间,因此使用long类型。
为便于创建雨滴,我们写一个方法叫spawnRaindrop(),它实例化一个新的Rectangle,把它设置到屏幕顶部的一个随机位置,并添加到raindrops数组。
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 = new Array<Rectangle>(); spawnRaindrop();
接下来在render()方法中添加几行,来检查自从产生一个新雨滴以来所经历的时间,如果需要的话再创建一个新雨滴:
if(TimeUtils.nanoTime() - lastDropTime > 1000000000) spawnRaindrop();
我们也需要让雨滴动起来,让我们采取简单的方法,让它们以每秒 200像素/单位 的恒定速度移动。如果雨滴位置屏幕底部以下,我们就从数组里移除它。
Iterator<Rectangle> iter = raindrops.iterator(); while(iter.hasNext()) { Rectangle raindrop = iter.next(); raindrop.y -= 200 * Gdx.graphics.getDeltaTime(); if(raindrop.y + 64 < 0) iter.remove(); }
雨滴需要渲染。在SpriteBatch中添加渲染代码后如下:
batch.begin(); batch.draw(bucketImage, bucket.x, bucket.y); for(Rectangle raindrop: raindrops) { batch.draw(dropImage, raindrop.x, raindrop.y); } batch.end();
最后一个调整:如果雨滴碰到了水桶,我们希望播放下雨声并从数组移动该雨滴。我们简单地向雨滴循环更新处添加下面几行:
if(raindrop.overlaps(bucket)) { dropSound.play(); iter.remove(); }
Rectangle.overlaps() 方法检查是否一个矩形和另一个矩形重叠。在此例中,我们让下雨音效播放并从数组移除该雨滴。
用户可以在任何时候关闭应用。对这个简单的例子而言没什么要处理的。然而,通常来说帮助操作系统收拾残局是一个很好的主意。
任何实现了Disposable接口的libgdx类并且因此带有adispose()方法,都需要在不使用时手动销毁。在我们的例子中纹理,声音,音乐和SpriteBatch都是符合条件类。身为好市民,我们如下这样实现{ApplicationListener#dispose() 方法:
@Override public void dispose() { dropImage.dispose(); bucketImage.dispose(); dropSound.dispose(); rainMusic.dispose(); batch.dispose(); }
一旦你销毁一个资源,就不可以在任何地方访问它。
可销毁资源通常是一些不能被Java的垃圾回收器处理的本地资源。这就是我们为什么要手动销毁的原因。Libgdx 提供丰富的方法来管理资源。读剩下的开发手册查看这些方法。
每次用户接到一个电话或按下home键时,Android都 有暂停和恢复应用程序的标记。Libgdx在这种情况下会自动为你做很多事情,比如:重新加载可能丢失的图片(OpenGL上下文丢失,对它而言是很严重的一个话题),暂停和恢复音乐流等。
我们的游戏其实不需要处理暂停和恢复。当用户回到应用时,游戏会接着上次离开时的状态继续运行。通常人们会实现一个暂停按钮让用户点击屏幕继续。这留给读者作为练习。查看下ApplicationListener.pause()和ApplicationListener.resume()方法。
这是我们这个简单游戏的源码:
package com.badlogic.drop; import java.util.Iterator; import com.badlogic.gdx.ApplicationListener; 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.GL10; 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.TimeUtils; public class Drop implements ApplicationListener { Texture dropImage; Texture bucketImage; Sound dropSound; Music rainMusic; SpriteBatch batch; OrthographicCamera camera; Rectangle bucket; Array<Rectangle> raindrops; 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<Rectangle>(); 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 glClearColor are the red, green // blue and alpha component in the range [0,1] // of the color to be used to clear the screen. Gdx.gl.glClearColor(0, 0, 0.2f, 1); Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT); // 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; if(bucket.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 later case we play back // a sound effect as well. Iterator<Rectangle> iter = raindrops.iterator(); while(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(); } @Override public void resize(int width, int height) { } @Override public void pause() { } @Override public void resume() { } }
这是一个非常基础的例子,展示怎样用Libgdx创建一个简易的游戏。一些东西还可以再改进,比如使用Pool类来循环使用Rectangles,我们在删除雨滴时让垃圾回收器都把它们回收了。如果一个批处理中给它许多不同图片,OpenGL就不好用。在我们的例子中没问题,因为我们只有两个图片。通常我们会把所有不同图片放在单个Texture里,也被称作TextureAtlas。
我强烈推荐你读剩下的开发手册,并检出Git仓库里的demo和测试。编程快乐。