本章源码链接:http://pan.baidu.com/s/1sjYE0sH 密码:q4n2
正如标题上所标注的,本章将完成FlappyBird的所有剩余内容。对比原版游戏我们可以发现FlappyBird现在还差两个界面,如下所示:
LibGDX提供Game和Screen两个类使我们可以将游戏拆分为多个界面。其中Game实现了ApplicationListener接口,所以他也可以作为项目的主类(也就是共享项目的启动类)。而Screen是LibGDX提供的一个具有完整生命周期的界面类。Game类提供了一个setScreen()方法可以让我们切换界面。
现在我们将游戏拆分为三个界面,分别是欢迎界面(WelcomeScreen)、预览界面(PreviewScreen),这两个界面分别对应上述两张截图,最后一个界面就是我们的游戏开始界面(FlappyBirdScreen)。那么游戏主类就是继承于Game的一个类了,这里我们命名为FlappyBirdGame。
在欢迎界面我们有两个目的,第一个目的是显示一个具有标志性的logo,第二个目的是加载资源。这两个目的是相辅相成的,我们知道,当资源过多时从硬盘加载到内存是需要相当大的开销,如果我们直接加载的话,屏幕可以能会出现短暂的黑屏甚至可能导致ANR(应用程序无响应),这样的情况对于用户来说是一种极为糟糕的体验。可能你已经发现前面我们一直使用的是直接加载应用资源。接下来我们将实现异步加载应用,并且在加载资源的这段时间里我们会在屏幕上显示一个标志性的logo背景,这样就可以完美的避免ANR和糟糕的体验。
首先让我们改造一下Assets类,让我们可以异步加载资源:
public class Assets implements Disposable, AssetErrorListener {
...
public void init (AssetManager assetManager) {
this.assetManager = assetManager;
// 设定资源管理器的错误处理对象句柄
assetManager.setErrorListener(this);
// 载入纹理集
assetManager.load(Constants.TEXTURE_ATLAS_OBJECTS, TextureAtlas.class);
// 载入声音文件
assetManager.load("sounds/sfx_die.ogg", Sound.class);
assetManager.load("sounds/sfx_hit.ogg", Sound.class);
assetManager.load("sounds/sfx_point.ogg", Sound.class);
assetManager.load("sounds/sfx_swooshing.ogg", Sound.class);
assetManager.load("sounds/sfx_wing.ogg", Sound.class);
}
public boolean isLoaded() {
if(!assetManager.update()) return false;
// 打印资源信息
Gdx.app.debug(TAG, "# of assets loaded: " + assetManager.getAssetNames().size);
for(String a : assetManager.getAssetNames()) {
Gdx.app.debug(TAG, "asset: " + a);
}
atlas = assetManager.get(Constants.TEXTURE_ATLAS_OBJECTS, TextureAtlas.class);
fonts = new AssetFonts();
bird = new AssetBird(atlas);
pipe = new AssetPipe(atlas);
land = new AssetLand(atlas);
number = new AssetNumber(atlas);
assetUI = new AssetUI(atlas);
sounds = new AssetSounds(assetManager);
decoration = new AssetDecoration(atlas);
return true;
}
...
}
我们发现我们删除了init()方法中加载资源和创建分类资源的代码,剩下的代码仅仅是预加载资源而已。这里我们添加了一个isLoaded()方法,该方法具有两个功能,第一是判断资源是否加载成功,第二是创建分类资源对象。在isLoaded()中,我们首先调用AssetManager的update()方法,该方法也具有两个功能,第一是触发资源开始加载,并且是异步触发,也就死说update()方法不会阻塞当前线程,第二个是判断资源是否加载成功。所以当我们第一次调用isLoaded()的时候触发了资源开始异步加载,接着后面每次调用isLoaded()都会判断资源是否加载成功,如果成功则创建分类资源对象,然后返回true,否则返回false。
接下来我们创建第一个界面WelcomeScreen:
package com.art.zok.flappybird;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Screen;
import com.badlogic.gdx.graphics.Texture;
import com.art.zok.flappybird.game.Assets;
import com.badlogic.gdx.assets.AssetManager;
import com.badlogic.gdx.scenes.scene2d.Stage;
import com.badlogic.gdx.scenes.scene2d.Action;
import com.badlogic.gdx.scenes.scene2d.ui.Image;
import com.badlogic.gdx.utils.viewport.StretchViewport;
import com.badlogic.gdx.scenes.scene2d.actions.Actions;
public class WelcomeScreen implements Screen {
private Stage stage;
private FlappyBirdGame game;
public WelcomeScreen(FlappyBirdGame game) {
this.game = game;
}
@Override
public void show() {
Image background = new Image(new Texture(Gdx.files.internal("images/splash.png")));
background.setSize(Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
Action fadeIn = Actions.fadeIn(1);
background.addAction(fadeIn);
stage = new Stage(new StretchViewport(
Gdx.graphics.getWidth(),
Gdx.graphics.getHeight()));
stage.addActor(background);
// 预加载资源
Assets.instance.init(new AssetManager());
}
@Override
public void hide() {
stage.dispose();
}
private void update() {
if(Assets.instance.isLoaded())
game.setScreen(new PreviewScreen(game));
}
@Override
public void render(float delta) {
update();
stage.act();
stage.draw();
}
...
}
首先我们的构造函数需要一个主类对象作为参数,我们保存该参数是为了在恰当的时间切换界面。现在需要着重关注三个重要的重写方法show()、hide()、render(),render方法不用多说,因为我们见的太多了,而且都具有相同的功能。主要是show()和hide(),show()方法在我们使用Game类的setScreen()方法中调用,也就是我们准备显示该界面的时候调用一次,因此该方法是我们实现初始化的最佳地方。hide()方法其实也是在show()方法中调用,但不同的是,hide()方法是在切换第二个界面时调用的,也就是说当我们每次调用setScreen()方法时,Game类都会提前先掉用当前保存的Screen类对象的hide()方法,所以hide()方法非常合适释放内存。
根据上述分析,我们在show()中创建一个背景图片Image对象和Stage对象,并为Image对象添加一个渐入动作类,最后我们进行资源的预加载。
hide()方法我们释放了stage对象。
update()方法中我们触发并判断资源是否加载成功,如果成功则切换到下一个界面(PreviewScreen)。render()方法
中调用了update()并渲染了stage对象。
在创建第二个界面之前我们需要修改一下Bird对象,这是为什么呢,因为在预览界面我们也需要使用Bird对象,但是预览界面和游戏界面Bird对象在屏幕的位置完全不同,因此为了能让Bird支持在不同的位置显示,我们必须改造其构造方法和初始化方法:
...
public Bird(float x, float y) {
init((int) (Math.random() * 3), x, y);
}
// 初始化
public void init(int selected, float x, float y) {
super.init(); // 保证对父类的初始化
contacted = false;
flashed = false;
if (selected == 1) {
birds = Assets.instance.bird.bird0;
} else if (selected == 2) {
birds = Assets.instance.bird.bird1;
} else {
birds = Assets.instance.bird.bird2;
}
birdAnimation = new Animation(0.1f, birds);
birdAnimation.setPlayMode(Animation.PlayMode.LOOP);
dimension.set(3.72f, 2.64f);
position.set(x, y);
}
...
我们为构造方法和初始化方法分别添加了两个float类型数据表示Bird的初始化位置,最后我们将x和y设置为position的值。既然修改了Bird构造函数,那么我们就必须修改WorldController的initBird()方法:
private void initBird() {
if(bird == null) {
bird = new Bird(-4.5f, 1.3f);
} else {
bird.init((int) (Math.random() * 3), -4.5f, 1.3f);
}
}
接下来我们创建第二个界面:
package com.art.zok.flappybird;
import com.art.zok.flappybird.game.Assets;
import com.art.zok.flappybird.game.UI.CustomButton;
import com.art.zok.flappybird.game.object.Bird;
import com.art.zok.flappybird.game.object.Land;
import com.art.zok.flappybird.util.Constants;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Screen;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.g2d.TextureAtlas.AtlasRegion;
import com.badlogic.gdx.scenes.scene2d.InputEvent;
import com.badlogic.gdx.scenes.scene2d.InputListener;
import com.badlogic.gdx.scenes.scene2d.Stage;
import com.badlogic.gdx.scenes.scene2d.ui.Image;
import com.badlogic.gdx.utils.viewport.StretchViewport;
public class PreviewScreen implements Screen {
private Bird bird; // bird object
private Land land; // land object
private Stage stage; // stage
private Image title; // text title image
private Image copyRight; // text copyright image
private float viewWidth; // last viewport width
private SpriteBatch batch; // sprite render batch
private FlappyBirdGame game; // main class
private AtlasRegion background; // random background texture region
private OrthographicCamera camera; // 正交投影相机
private CustomButton play, score, rate; // three buttons
public PreviewScreen(FlappyBirdGame game) {
this.game = game;
}
@Override
public void show() {
batch = new SpriteBatch();
viewWidth = Constants.VIEWPORT_HEIGHT * Gdx.graphics.getWidth() / Gdx.graphics.getHeight();
// bird and land
bird = new Bird(0, 2);
land = new Land();
// camera
camera = new OrthographicCamera(Constants.VIEWPORT_WIDTH, Constants.VIEWPORT_HEIGHT);
camera.position.set(0, 0, 0);
camera.update();
// stage
stage = new Stage(new StretchViewport(Constants.VIEWPORT_GUI_WIDTH,Constants.VIEWPORT_GUI_HEIGHT));
// background texture region
background = Assets.instance.decoration.bg.random();
// title image
title = new Image(Assets.instance.assetUI.textTitle);
title.setBounds(93, 553, 295, 90);
// copyright image
copyRight = new Image(Assets.instance.assetUI.copyRight);
copyRight.setBounds(137, 110, 206, 21);
// three buttons
play = new CustomButton(Assets.instance.assetUI.buttonPlay);
play.setBounds(43, 175, 173, 108);
score = new CustomButton(Assets.instance.assetUI.buttonScore);
score.setBounds(263, 175, 173, 108);
rate = new CustomButton(Assets.instance.assetUI.buttonRate);
rate.setBounds(189, 313, 102, 65);
stage.addActor(title);
stage.addActor(copyRight);
stage.addActor(play);
stage.addActor(score);
stage.addActor(rate);
//input handler
Gdx.input.setInputProcessor(stage);
// touch up event (touchDown must be return true)
play.addListener(new InputListener() {
public boolean touchDown(InputEvent event, float x, float y, int pointer, int button) {
return true;
};
public void touchUp(InputEvent event, float x, float y, int pointer, int button) {
game.setScreen(new FlappyBirdScreen());
}
});
}
@Override
public void hide() {
batch.dispose();
stage.dispose();
}
private void update(float deltaTime) {
bird.update(deltaTime); // wave animation
land.update(deltaTime); // move animation
}
@Override
public void render(float delta) {
update(Gdx.graphics.getDeltaTime());
Gdx.gl.glClearColor(0f, 0f, 0f, 1f);
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
batch.setProjectionMatrix(camera.combined);
batch.begin();
batch.draw(background, -viewWidth / 2, -Constants.VIEWPORT_HEIGHT / 2, viewWidth, Constants.VIEWPORT_HEIGHT);
bird.render(batch);
land.render(batch);
batch.end();
stage.act();
stage.draw();
}
@Override
public void resize(int width, int height) {
camera.viewportWidth = Constants.VIEWPORT_HEIGHT * width / height;
camera.update();
}
@Override
public void pause() {
}
@Override
public void resume() {
}
@Override
public void dispose() {
}
}
虽然PreviewScreen类中添加的东西比较多,但是并没有什么难以理解的内容。我们可以简单的介绍一下实现思路,首先我们维护了一个Stage对象用于承载相应的组件对象,如文本图片(“FlappyBird”)、开始按钮(play)、分数按钮(score)、还有速度按钮(rate)和版权图片。接着我们创建了一个Bird对象和一个Land对象,然后创建了一个相机对象,该对象用于渲染Bird和Land,并且我添加了update()方法更新Bird和Land对象。最后我们在hide()中释放了batch和stage两个对象。
最重要的是我们为play按钮添加了点击监听器,如果点击该按钮我们就切换到游戏界面。
接下来,我们创建FlappyBirdScreen界面:
package com.art.zok.flappybird;
import com.art.zok.flappybird.game.WorldController;
import com.art.zok.flappybird.game.WorldRenderer;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Screen;
import com.badlogic.gdx.graphics.GL20;
public class FlappyBirdScreen implements Screen {
private WorldController worldController;
private WorldRenderer worldRenderer;
public boolean paused;
@Override
public void show() {
paused = false;
worldController = new WorldController(this);
worldRenderer = new WorldRenderer(worldController);
}
@Override
public void hide() {
worldController.dispose();
worldRenderer.dispose();
}
@Override
public void render(float delta) {
if(!paused) {
float deltaTime = Math.min(Gdx.graphics.getDeltaTime(), 0.018f);
worldController.update(deltaTime);
}
Gdx.gl.glClearColor(0x64/255.0f, 0x95/255.0f, 0xed/255.0f, 0xff/255.0f);
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
worldRenderer.render();
}
@Override
public void resize(int width, int height) {
worldRenderer.resize(width, height);
}
@Override
public void pause() {
paused = true;
}
@Override
public void resume() {
paused = false;
}
@Override
public void dispose() {
}
}
上述的类和之前的FlappyBirdMain类几乎一样,稍有不同的是我们将初始化放在了show()方法中,将对象释放的代码放在了hide()方法中。并且我们不在resume()中重新加载资源了。我们之前说Android在应用进入后台后可能会释放部分资源,所以在重返前台时必须重新加载资源,但是这里指的是我们直接使用文件句柄加载资源的情况,如果我们使用AssetManger加载并管理资源就不需要担心这个问题了。
最后,我们需要创建FlappyBirdGame类:
package com.art.zok.flappybird;
import com.art.zok.flappybird.game.Assets;
import com.badlogic.gdx.Game;
public class FlappyBirdGame extends Game {
@Override
public void create() {
setScreen(new WelcomeScreen(this));
}
@Override
public void dispose() {
super.dispose();
Assets.instance.dispose();
}
}
该类的初始化化函数依然是create(),所以我们只是在create()初始化界面为欢迎界面,然后重载dispose()方法保证游戏退出时资源的释放。
添加音频响应
最后我们要为应用添加声音。首先我们这里加载了五种声音资源,分别是die、hit、point、swooshing和wind。这五种资源分别是在应用死亡、碰撞、得分、按钮按下和煽动翅膀的时候触发,下面我们分别添加五种声音。
首先是die和hit在碰撞那一瞬间就应该触发,所以我们应该在Bird的contact()方法中播放hit和die:
// 通知碰撞事件
public void contact() {
contacted = true;
Assets.instance.sounds.hit.play();
Assets.instance.sounds.die.play();
}
小鸟煽动翅膀应该是在每次触发跳跃时:
// 触发跳跃
public void setJumping() {
if(body != null) {
body.setLinearVelocity(0, 35f);
Assets.instance.sounds.wing.play();
}
}
下面是point,point应该在WorldController的score增加1时播放,所以:
// 计算分数
private void calculateScore () {
for(Pipe pipe : pipes.pipes) {
if(pipe.position.x < bird.position.x) {
if(pipe.getScore() == 1) {
score += 1;
Assets.instance.sounds.point.play();
Gdx.app.debug(TAG, "Your current score:" + score);
}
}
}
}
最后是swooshing应该在每个自定义按钮被按下时调用,所以修改CustomButton:
public class CustomButton extends Button {
...
public CustomButton(AtlasRegion reg) {
...
addListener(new InputListener() {
@Override
public boolean touchDown(InputEvent event, float x, float y,
int pointer, int button) {
Assets.instance.sounds.swooshing.play();
return super.touchDown(event, x, y, pointer, button);
}
});
}
我们在CustomButton的构造函数添加了一个监听器,并重写了touchDown方法,在该方法中我们只是播放了swooshing声音,这样凡事继承于CustomButton的按钮都会在按下时播放该声音。
到现在,我们的游戏FlappyBird已经全部完成了,现在可以进行一次完整的测试了。
下面是测试视频链接:
http://pan.baidu.com/s/1hryFmoW
谢谢大家,我用了八章的篇幅介绍了如何使用LibGDX以及内置的BOX2D引擎重建FlappyBird游戏,在部分内容上可以并不是最佳的实现方法,所以希望大家不吝赐教,文中有所疏漏在所难免,希望各位读者能及时指出,本人将不胜感激。转载于:https://my.oschina.net/u/2432369/blog/610410