首页 > 代码库 > A simple libgdx game (一个简单的游戏)

A simple libgdx game (一个简单的游戏)

在深入钻研libGDX提供的api之前,让我们创建一个简单的小游戏来初步接触一个每个模块。这里将会主要介绍一些设计思想,而非细节。
我们将会看到如下内容:
1.主要的文件操作
2.清屏
3.绘制图片
4.使用相机
5.主要的输入处理
6.播放声音效果
工程的创建就不在赘述了。
The Game (游戏)
游戏的idea很简单:
1.用桶抓住雨滴
2.桶在屏幕的下方
3.雨滴在屏幕的上面随机出现并且垂直下落
4.玩家可以通过鼠标或者触摸或者键盘方向键来水平移动桶
5.游戏没有结束条件。。。

The Assets (资源)

使用到的资源有水滴,桶,还有声音文件。这些文件需要被放在工程目录下面的assets文件夹里。

Configuring the Starter Classes (配置启动者类)

桌面项目中Main.java配置如下:
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 config = new LwjglApplicationConfiguration();
      config.title = "Drop";
      config.width = 800;
      config.height = 480;
      new LwjglApplication(new Drop(), config);
   }
}
上面代码配置了config对象的属性,窗口的大小为480*800,标题为“Drop”。
然后再来看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="8" android:targetSdkVersion="19" />
    <uses-feature android:glEsVersion="0x00020000" android:required="true" />

    <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:screenOritation被设置成了“landscape”,如果要让游戏运行在竖屏模式,那么就需要设置这个属性值为“portrait”。
我们还想能够保护电池,所以要让加速计和指南正不可用。我们在AndroidLauncher.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 AndroidLauncher extends AndroidApplication {
   @Override
   public void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);

      AndroidApplicationConfiguration config= new AndroidApplicationConfiguration();
      config.useAccelerometer = false; //设置加速计不可用
      config.useCompass = false;  //设置指南正不可用

      initialize(new Drop(), config);
   }
}
我们不能定义Acitivity的分辨率,因为它是Android系统设置的。

The Code (代码)

我们把代码分成几个部分讲解。为了简单化,我们把所有的代码都放在了核心项目的Drop.java文件中。

Loading the Assets (载入资源)

我们的第一个任务是要载入资源并且保存他们的引用。资源通常在ApplicationListener.create()方法中载入。所以我们这样做:
public class Drop implements ApplicationListener {
   private Texture dropImage;
   private Texture bucketImage;
   private Sound dropSound;
   private 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类中都有一个变量,所以我们以后可以随时使用它。最开始两行加载了两张图片。一个Texture代表了一个被夹在到图像随机存储器中的图片。一个Texture可以通过向t它的构造函数传递一个指向某个资源文件的FileHandle来加载。这个FileHandle实例通过Gdx.files中的方法获得。有不多不同的文件种类,我们使用“internal”文件类型来指向我们的资源。内部文件就是在Android工程中assets文件下的资源。
下一步,我们加载了音效和背景音乐,liGDX认为sound和music是不同的,sound被存储到内存中,而music不管是否被存储都被当做流来处理。Music通常太大而不能再内从中完全存储。作为一个不成文的规矩,你应该让小于10秒的声音资源是一个Sound,而更长的资源是一个music。
sound和music的加载是通过Gdx.audio.newSound()和Gdx.audio.newMusic()完成的。这两个方法都使用了FileHandle。
在create(0方法的最后,我们设置了music实例循环播放,并且让它立刻播放。

A camera and aSpriteBatch (摄像机和画笔)

下一步,我们要创建一个摄像机和SpriteBatch。我们将使用前者来实现无论设备的屏幕分辨率是多少,我们都是用480*800的分辨率来绘制。SpriteBatch是一个用来绘制2D图片的类,比如绘制我们加载的Texture。
添加两个变量:
private OrthographicCamera camera;
   private SpriteBatch batch;
在create()方法中,我们首先创建相机如下:
camera = new OrthographicCamera();
   camera.setToOrtho(false, 800, 480);
这将保证相机始终向我们展示一个拥有480*800个单元的一个区域。可以把它想象成一个虚拟的世界。我们先把这些个单元叫做像素,来方便后面的开发。这样就没有什么能阻止我们使用使用其他的单元。Cameras是一个非常给力的工具,你可以使用它来做很多事,这里不做太多的介绍。
下一步,创建一个SpriteBatch,也是在create()方法中。
batch = new SpriteBatch();
这样我们就几乎完成了游戏所需要的所有的初始化操作。

Adding the Bucket (添加桶)

最后我们就要让桶和雨滴表现出来。让我们来想象在代码中需要表现出什么:
1.他们需要有一个在我们的480*800的世界中的坐标位置。
2.他们需要有宽和高
3.他们需要有一个图形来显示,即我们加载的texture
所以,要描述他们,我们需要保存他们的位置和大小。libGDX提供了一个rectangle类,它可以让我们达到目的。添加一个新的变量:
private Rectangle bucket;
在create()方法中,我们实例化它,并且指定它的初始化信息。
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中一样)都是以左下角为起点。

Rendering the Bucket(渲染桶)

是时候渲染桶了。我们要做的第一件事是使用蓝黑色清空屏幕:
@Override
   public void render() {
      Gdx.gl.glClearColor(0, 0, 0.2f, 1);
      Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);

      ... more to come here ...
   }
这两行是唯一关于OpenGL你所要知道的。第一句话我们设置清空颜色为蓝色。参数为argb,每一个值范围在0-1之间。下一句话让OpenGL真正的来清空屏幕。
下一步,我们需要告诉相机来确保它更新了。相机使用了数学上的矩阵来描述绘制的坐标系统。这些矩阵需要在我们改变相机参数的时候进行从新计算,比如改变它的坐标。我们不在这个简单的例子中做这件事了,但是在每一帧来更新相机是很好的事。
camera.update();
现在,我们可以绘制桶了:
batch.setProjectionMatrix(camera.combined);
   batch.begin();
   batch.draw(bucketImage, bucket.x, bucket.y);
   batch.end();
第一句话告诉了SpriteBatch使用相机的坐标系统。像上面描述的一样,这是通过一个叫矩阵的东西完成的,更明确一点,是一个投影矩阵。camera.combined变量就是一个矩阵。此后,SpriteBatch就将绘制前面在坐标系统中所描述的一切东西。
下一步,我们告诉SpriteBatch来开始一个新的批次。为什么我们需要这个,还有什么是batch?
OpenGL 最讨厌的就是告诉它一个一个图片。它想被一次被告诉尽可能多的图片。
SpriteBatch类让OpenGL感到高兴。它将记录在SpriteBatch.begin()和SpriteBatch.end()直接的所有绘制请求,可以在一定程度上加快渲染效率。这开始的时候看起来很笨重,但是如果在每秒60帧渲染500个精灵的情况下和在每秒20帧渲染100个精灵的情况下,就能够看出差别了。
Making the Bucket Move (Keyboard)通过键盘让桶移动
在桌面和浏览器游戏中,我们能够接受键盘输入。我们让桶能够在键盘上左右方向键被按下的时候让桶来移动。
我们让桶的移动不具有加速度,移动速度为每秒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()方发返回了上一帧和当前帧的时间间隔。
保证桶在屏幕范围内
if(bucket.x < 0) bucket.x = 0;
if(bucket.x > 800 - 64) bucket.x = 800 - 64;

Adding the Raindrops (添加雨滴)

对于雨滴,我们使用了一个Rectangle的List集合,保存了每一个雨滴的位置和大小。添加变量:
private Array<Rectangle> raindrops;
Array类是libGDX提供的工具来,它代替了java中标准的ArrayList集合。后者的问题是它会以不同的方式产生垃圾。Array类则尽可能的最小化垃圾的产生。liGDX也提供了许多其他的能够回收垃圾的集合,比如hash-maps和sets。
我们还需要记录我们产生一个雨滴的最后时间,所以添加一个变量:
private 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();
   }
雨滴也需要渲染:
 batch.begin();
   batch.draw(bucketImage, bucket.x, bucket.y);
   for(Rectangle raindrop: raindrops) {
      batch.draw(dropImage, raindrop.x, raindrop.y);
   }
   batch.end();
最后一道工序:如果雨滴碰撞到了桶,我们要播放drop音效并且把雨滴从集合中删除:
if(raindrop.overlaps(bucket)) {
         dropSound.play();
         iter.remove();
 }
Rectangle.overlaps()方法检测一个rectangle是否和另一个rectangle有重叠的部分。

Clieaning Up (清理)

用户可以在任何时候关闭游戏。对于这个简单的例子,没有什么需要做的。然而,帮助操作系统清理我们产生的混乱总是好的。
在libGDX中任何一个实现了Disposable接口的类都有一个dispose()方法来在它不需要再使用的时候释放掉它占用的资源。所以我们实现了ApplicationListener#dispose()方法:
@Override
   public void dispose() {
      dropImage.dispose();
      bucketImage.dispose();
      dropSound.dispose();
      rainMusic.dispose();
      batch.dispose();
   }
一旦释放了一个资源,就不能再使用它了。
可处理的废物资源通常是本地资源,这些资源不能够被java的垃圾回收期处理。这就是为什么我们需要手动清理它们的原因。

Handling Pausing/Resuming (处理暂停、继续)

Android在每次用户有电话进来或者按下了Home键的时候都有暂停和继续的标记。ligGDX会在这种情况下为你自动做很多事情,比如重新加载可能丢失的资源,暂停或者继续音频流等等。
在我们的游戏当中,我们不需要处理暂停和继续。只要用户再次回到游戏中,游戏就会从离开的地方继续运行。

The Full Source (全部代码)

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.GL20;
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 {
   private Texture dropImage;
   private Texture bucketImage;
   private Sound dropSound;
   private Music rainMusic;
   private SpriteBatch batch;
   private OrthographicCamera camera;
   private Rectangle bucket;
   private Array<Rectangle> 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<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(GL20.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() {
   }
}