首页 > 代码库 > 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() { } }
声明:以上内容来自用户投稿及互联网公开渠道收集整理发布,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任,若内容有误或涉及侵权可进行投诉: 投诉/举报 工作人员会在5个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。