首页 > 技术文章 > First Demo - Step 1

mazing 2020-07-20 13:10 原文

在深入了解LibGDX详细的API之前,让我们先创建一个简单的游戏,它会对每个模块都有接触,我们将介绍一些概念:

  1. 资源文件访问
  2. 清除屏幕
  3. 绘制图像
  4. 使用相机
  5. 基本输入处理
  6. 播放声音

游戏设计

  1. 用水桶捕捉雨滴;
  2. 桶位于屏幕的下部;
  3. 雨滴每秒钟随机产生屏幕顶部,并向下加速;
  4. 玩家可以通过鼠标/触摸水平拖动水桶。

 

游戏资源

要使游戏资源可用于游戏,我们必须将它们放在Android资产文件夹中。 我命名了4个文件:drop.wav,rain.mp3,drops.png和bucket.png,并将它们放在android / assets /中。
 
配置启动类
为了方便调试,我们先打开桌面项目的启动类:Desktoplauncher.java,并加入3行config代码
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);
   }
}

 

为了节约手机电量,我们打开Android的启动类AndroidLauncher.java,插入两行config代码

import android.os.Bundle;

import com.badlogic.gdx.backends.android.AndroidApplication;
import com.badlogic.gdx.backends.android.AndroidApplicationConfiguration;
import com.badlogic.drop.Drop;

public class AndroidLauncher extends AndroidApplication {
   @Override
   protected void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      AndroidApplicationConfiguration config = new AndroidApplicationConfiguration();
      config.useAccelerometer = false;
      config.useCompass = false;
      initialize(new Drop(), config);
   }
}

 

加载资源文件

我们的第一个任务是加载资产并存储对它们的引用。 资源通常在create()方法中加载。对于每个资源文件,在Drop.java中都有对应的变量指向它,这样我们就可以在后期随时随地使用它。

create()方法中的前两行加载雨滴和桶的图像。纹理表示存储在Video Ram中的图像,通常传递一个来自Assets的资源文件的FileHandle句柄到Texture 来创建一个纹理,FileHandle 实例是通过Gdx.files提供的方法之一获得的。我们使用【internal】来获得Assets文件夹下的资源文件,internal 文件位于Android项目的 assets 目录中。 如前所述,桌面和HTML5项目引用同一目录。

接下来我们加载声音效果和背景音乐。Music 通常比较大。根据经验,如果您的声音资源文件短于10秒,您应该使用Sound实例,而更长音频片段则使用Music实例。

声音或音乐实例的加载是通过Gdx.audio.newSound()和Gdx.audio.newMusic()完成的。 这两种方法都采用FileHandle,就像Texture构造函数一样。

import com.badlogic.gdx.ApplicationAdapter;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.audio.Music;
import com.badlogic.gdx.audio.Sound;
import com.badlogic.gdx.graphics.Texture;

public class Drop extends ApplicationAdapter {
   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();
   }

 

相机和SpriteBatch

接下来,我们要创建一个摄像头和一个SpriteBatch。 我们将使用前者(Camera)来确保我们可以使用800x480像素的目标分辨率渲染,无论实际的屏幕分辨率如何。 SpriteBatch是一个特殊的类,用于绘制2D图像,像加载我们的纹理。

我们在类上添加两个新的字段,我们称之为相机和batch:

 private OrthographicCamera camera;
 private SpriteBatch batch;

 

在create()方法中,我们这样初始化相机

camera = new OrthographicCamera();
camera.setToOrtho(false, 800, 480);

 

这将确保相机总是向我们展示我们的游戏世界的800x480单位宽的区域。 把它看作是我们世界的虚拟窗户。 我们目前将单位默认 为像素。 没有什么可以阻止我们使用其他单位, 米或任何你可以想得到的单位。 相机非常强大,您可以在本基础教程中做很多事情。查看开发者指南的其余部分了解更多信息。

接下来我们仍在create()方法中创建SpriteBatch实例:

batch = new SpriteBatch();

 

添加桶

接下来我们来添加我们的桶还有雨滴:
  1. 一个桶/雨滴在我们的800x480单位世界的x / y位置。
  2. 一个桶/雨滴的宽度和高度以我们世界的单位表示。
  3. 桶/雨滴具有图形表示,我们已经具有我们加载的Texture实例的形式。

所以,要描述桶和雨滴,我们需要存储他们的位置和大小。 Libgdx提供了一个可以用于此目的的Rectangle类。 我们首先创建一个代表我们的存储桶的Rectangle类。 我们添加一个新字段:

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个像素上方。 等等,为什么bucket.y设置为20,不应该是480 - 20?这是因为LibGDX采用的是笛卡尔坐标系(以左下角为原点)。

 

渲染桶

是时候来绘制我们的桶了。我们要做的第一件事是用深蓝色来清除屏幕。 只需将render()方法更改为如下所示:
@Override
   public void render() {
      Gdx.gl.glClearColor(0, 0, 0.2f, 1);
      Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
   }
如果您使用高级类(如Texture或SpriteBatch),这两行就是您需要了解OpenGL的唯一内容。 第一个调用会将清屏的颜色设置为蓝色。 参数是该颜色的红色,绿色,蓝色和alpha值,分别在[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开始一个新批量渲染。 为什么我们需要这个,什么是批量渲染? OpenGL讨厌不高效的绘制渲染。 它希望被告知有关尽可能多的图像需要渲染,并一次性渲染完毕。

SpriteBatch 使得 OpenGL 在渲染视图上更加得心应手,它将记录SpriteBatch.begin()和SpriteBatch.end()之间的所有绘图命令。 一旦我们调用SpriteBatch.end(),它将一次提交我们所做的所有绘图请求,加快渲染速度。 这一切可能在一开始就看起来很麻烦,但是它使得渲染500个精灵之间的差异是每秒60帧,并以每秒20帧的速度渲染100个精灵。

 

使水桶移动

时间让用户控制桶。 之前我们说过我们允许用户拖动存储桶。 让我们让事情变得更容易一些。 如果用户触摸屏幕(或按下鼠标按钮),我们希望水桶将水平放置在该位置的周围。 将以下代码添加到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作为参数。 我们创建这样一个向量,设置当前的触摸/鼠标坐标并调用方法。 矢量现在将包含我们的桶所在坐标系中的触摸/鼠标坐标。最后,我们改变桶的位置,以触摸/鼠标坐标为中心。

注意:总是实例化一个新的对象,如Vector3实例这是一个非常糟糕的做法。 因为垃圾收集器必须频繁地收集这些短命的变量。在桌面应用上可能还好,但是在Android上,GC可能会导致停留时间达几百毫秒,从而导致卡顿。 为了在这种特殊情况下解决这个问题,我们可以简单地让TouchPos成为Drop类的一个全局变量,而不是一直实例化它。
touchPos是一个三维向量。 你可能会想知道为什么我们只用2D操作但是使用一个三维向量。 OrthographicCamera实际上是一个3D相机,也考虑到z坐标。 想想CAD应用程序,他们也使用3D摄相机。 我们只是滥用它来绘制2D图形。
 

添加雨滴

对于雨滴,我们设计了一个Rectangle 列表实例,每个实例都跟踪雨滴的位置和大小。 我们将该列表添加为一个字段:

 
 private Array<Rectangle> 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()方法中,我们实例化了雨滴数组,并产生了我们的第一个雨滴:
我们需要在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();

一个最后的调整:如果一个雨滴撞到水桶,我们想播放我们的下落声音,并从阵列中删除雨滴。 我们只需将以下行添加到雨滴更新循环中:

if(raindrop.overlaps(bucket)) {
         dropSound.play();
         iter.remove();
      }

Rectangle.overlaps()方法检查这个矩形是否与另一个矩形重叠/碰撞。 在我们的情况下,我们将播放声音效果,并从阵列中删除雨滴。

释放资源
用户可以随时关闭应用程序。 对于这个简单的例子,没有什么需要做的。 然而一般来说,帮助操作系统清理我们创建的垃圾是一个好主意。

任何实现Disposable接口的libgdx类,都具有dispose()方法,在不再使用后需要手动进行清理。 在我们的例子中,纹理,声音和音乐以及SpriteBatch都是如此。 作为好的程序员,我们复写ApplicationAdapter.dispose()方法如下:

@Override
   public void dispose() {
      dropImage.dispose();
      bucketImage.dispose();
      dropSound.dispose();
      rainMusic.dispose();
      batch.dispose();
   }

处理资源后,您不应该以任何方式访问它。

Disposables (本机资源)通常是无法被Java垃圾收集器处理的。 这就是为什么我们需要手工处理它们的原因。 Libgdx提供了各种帮助资产管理的方式。 阅读开发指南的其余部分来发现它们。

处理暂停/恢复

每当用户打电话或按住主页按钮时,Android都会暂停和恢复您的应用程序。 在这种情况下,Libgdx会为您自动执行许多操作,例如 重新加载可能已经丢失的图像(OpenGL上下文丢失,一个可怕的主题),暂停和恢复音乐流等。

在我们的游戏中,实际上不需要处理暂停/恢复。 一旦用户回到应用程序,游戏就会继续下去。 通常会实现暂停屏幕,并要求用户触摸屏幕以继续。 这是为读者留下的一个练习 - 查看ApplicationAdapter.pause()和ApplicationAdapter.resume()方法。




推荐阅读