LibGDX重建Flappy Bird——(8)屏幕切换与播放声音(终结)

原文链接:https://my.oschina.net/u/2432369/blog/610410

  本章源码链接:http://pan.baidu.com/s/1sjYE0sH 密码:q4n2
  正如标题上所标注的,本章将完成FlappyBird的所有剩余内容。对比原版游戏我们可以发现FlappyBird现在还差两个界面,如下所示:

LibGDX重建Flappy Bird——(8)屏幕切换与播放声音(终结)LibGDX重建Flappy Bird——(8)屏幕切换与播放声音(终结)

  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

上一篇:LibGDX重建Flappy Bird——(1) 项目创建与导入


下一篇:LibGDX重建Flappy Bird——(4) 创建游戏对象