LibGDX重建Flappy Bird——(7) 添加GUI信息

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

  本章源码链接:http://pan.baidu.com/s/1hruBkgc密码:94iq 

  上一章介绍我们基本已经完成FlappyBird的游戏逻辑,接下来在本章将添加一些GUI信息,如分数、按钮等。

分数GUI

  首先我们需要为WorldController维护一个int值表示当前玩家获得的分数。修改WorldController并添加相应的方法:

...
public class WorldController extends InputAdapter implements Disposable {  
    ...
    public int score;
    ...
    private void init() { 
    	...
    	score = 0;
    	isStart = false;
    	...
    }  
    ...
    // 计算分数
	private void calculateScore () {
		for(Pipe pipe : pipes.pipes) {
			if(pipe.position.x < bird.position.x) {
				if(pipe.getScore() == 1) {
					score += 1;
					Gdx.app.debug(TAG, "Your current score:" + score);
				}
			}
		}
	}

    public void update(float deltaTime) {
    	...
    	calculateScore();
    }  
...
}

  首先我们添加了一个整型成员变量score并在init()初始化为0。接着添加了一个calculateScore()方法计算当前分数,该方法遍历所有Pipe对象并判断pipe对象x坐标是否小于Bird对象x坐标,如果小于则获得当前pipe分数并打印调试信息。最后我们在update()中调用该方法计算当前得分。   接下来我们为游戏添加分数的GUI,即在屏幕上方显示的当前分数。首先为WorldRenderer添加两个方法:
// 游戏GUI渲染方法
 	private void renderGui(SpriteBatch batch) {
 		batch.setProjectionMatrix(camera.combined);
 		batch.begin();
 		renderScore(batch);
 		batch.end();
 	}
 	
 	// 分数
 	private void renderScore(SpriteBatch batch) {
 		int[] score = Tools.splitInteger(worldController.score);
 		float h = 4.5f; 
 		float w = 0, totalWidth = 0;
 		for(int i = 0; i < score.length; i++) {
 			AtlasRegion reg = Assets.instance.number.numbers_font.get(score[i]);
 			w = h * reg.getRegionWidth() / reg.getRegionHeight();
 			totalWidth += w;
 		}
 		
 		float x = -totalWidth / 2;
 		float y = Constants.VIEWPORT_HEIGHT / 2 * 0.6f;
 		w = 0;
 		for(int i = 0; i < score.length; i++) {
 			AtlasRegion reg = Assets.instance.number.numbers_font.get(score[i]);
 			w = h * reg.getRegionWidth() / reg.getRegionHeight();
 			batch.draw(reg.getTexture(), x, y, w/2, h/2, w, h, 1, 1, 0,
 					reg.getRegionX(), reg.getRegionY(),
 					reg.getRegionWidth()-(i != (score.length - 1) ? 3 : 0),
 					reg.getRegionHeight(),false, false);
 		        x += w;
 		}
 	}
   上述方法中使用了一个新类Tools.splitInteger(int)方法,该方法是我们新创建的一个类的静态方法,其功能是将一个整数各个位拆分开来。接下来我们创建该类及其相应的方法:
package com.art.zok.flappybird.util;

public class Tools {
	public static int[] splitInteger(int i) {
		char[] chars = Integer.toString(i).toCharArray();
		int[] result = new int[chars.length]; 
		for(int j = 0; j < chars.length; j++) {
			result[j] = chars[j]- 48;
		}
		return result;
	}
}
  上面添加的方法都很简单,如果你有更好的算法或者技巧可以分享一下。现在我们可以测试一下应用:
LibGDX重建Flappy Bird——(7) 添加GUI信息   接下来为Bird添加最高分数的永久保存方法。 首先为Constants类添加两个常量:
// best score file
	public static final String BEST_SCORE_FILE = "BestScoreFile";
	// best score key
	public static final String BEST_SCORE_KEY = "best_score_key";
  第一个常量表示最高分数保存的文件名,第二个常量表示保存最高分数的键值。Libgdx为我们提供了一个非常方便的类来永久保存数据,如果我们希望将某个数据保存,只需为该值提供一个字符串常量的键值,其原理就类似于哈希表,下面我们看代码:
public class WorldController extends InputAdapter implements Disposable {  
    ...
    public Preferences prefs;
    ...
    private void init() { 
    	...
    	prefs = Gdx.app.getPreferences(Constants.BEST_SCORE_FILE);  
    }  
    
    ...
	private void saveBestScore() {
		int bestScore = prefs.getInteger(Constants.BEST_SCORE_KEY);
		if(bestScore < score) {
			prefs.putInteger(Constants.BEST_SCORE_KEY, score);
			prefs.flush();
		}
	}
    public void update(float deltaTime) {
    	if(!isGameOver) {
    		if(isGameOver = bird.isGameOver()) {
    			saveBestScore();
    			Gdx.app.debug(TAG, "GAME OVER!");
    		}
    	}
    	...
    }  
    
}
  首先我们创建了一个Preferences类成员变量,然后在初始化方法init()中通过Gdx.app模块获得一个Preferences对象,这里我们需要一个永久保存 的文件名作为参数,我们使用了常量BEST_SCORE_FILE。接下来我们添加了一个新方法saveBestScore()用来保存最高分数,并且在update()中,我们判断如果游戏结束则进行最高分数保存。   下面我们要创建GUI对话框,该对话框包含了开始界面和结束界面已经游戏中的暂停按钮,其中开始界面上包含一张教程图片和一张标题图片,结束界面包含了一张结束语照片和一个分数面板,该面板上包含了本局分数、奖牌和最高分数,对话框最后还包含了两个按钮用于重新开始游戏等等。   首先让我创建一个自定义按钮类CustomButton,该类继承了Button类,CustomButton类实现了只有一张图片的按钮,并且当按钮按下时图片将会向下偏移一定距离:
package com.art.zok.flappybird.game.UI;

import com.badlogic.gdx.graphics.g2d.Batch;
import com.badlogic.gdx.graphics.g2d.TextureAtlas.AtlasRegion;
import com.badlogic.gdx.scenes.scene2d.ui.Button;
import com.badlogic.gdx.scenes.scene2d.utils.TextureRegionDrawable;

public class CustomButton extends Button {
	AtlasRegion region;
	public CustomButton(AtlasRegion reg) {
		super(new TextureRegionDrawable(reg));
		this.region = reg;
	}
	
	@Override
	public void draw(Batch batch, float parentAlpha) {
		if(isPressed()) {
			batch.draw(region, getX(), getY() - 2f, getWidth(), getHeight());
		} else {
			super.draw(batch, parentAlpha);
		}
	}
}
  可以发现该类很简单,最关键的就是在render()方法中判断两种状态,如果按钮处于按下状态则偏移绘制,如果没有按下则使用默认的绘制方式。   下 面我们创建一个用于显示分数的动画Actor,该Actor有两种情况,一种是可以静态显示分数的情况,另一种是可以从0开始一直动画前进到目标分数的动画:
package com.art.zok.flappybird.game.UI;

import com.art.zok.flappybird.game.Assets;
import com.art.zok.flappybird.util.Tools;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.g2d.Batch;
import com.badlogic.gdx.graphics.g2d.TextureAtlas.AtlasRegion;
import com.badlogic.gdx.scenes.scene2d.Actor;
import com.badlogic.gdx.utils.Array;

public class ScoreActor extends Actor {
	private int score;
	private Array<AtlasRegion> allRegs;
	private float duration;
	private int curScore;
	private int[] curScores;
	private boolean isStatic;
	private float x, y;
	public ScoreActor(int i, float x, float y, boolean isSatic) {
		this.score = i;
		this.x = x;
		this.y = y;
		this.isStatic = isSatic;
		allRegs = Assets.instance.number.numbers_font;
		curScores = Tools.splitInteger(i);
	}
	
	public void update () {
		duration += Gdx.graphics.getDeltaTime();
		if(duration > 0.1f) {
			duration = 0;
			curScore += 1;
			if(curScore > score) {
				return;
			} else {
				curScores = Tools.splitInteger(curScore);
			}
		}
		
	}
	
	@Override
	public void draw(Batch batch, float parentAlpha) {
		if(!isStatic) {
			update();
		}
		float w = 0, all = 0;
		for(int i = curScores.length - 1; i >= 0; i--) {
			AtlasRegion reg = allRegs.get(curScores[i]);
			w = 45 * reg.getRegionWidth() / reg.getRegionHeight();	
			all += w;
			batch.draw(reg, x - all, y, w, 45);
		}
		
	}
}
  该类的构造函数包含四个参数,第一个参数是要现实的目标分数,第二个和第三个参数代表显示的坐标,第四个参数表示是通过静态显示还动态显示。在构造函数首先保存了传递进来的参数,接下来获得了数字纹理资源列表,最后我们使用之前添加的Tools类方法将目标分数拆分成数组。   接下 来我们先看draw()方法,如果是静态显示,则不调用update()方法直接绘制目标分数的纹理组。如果是动画的形式,则我们需要在渲染每帧时调用update()方法,在update()方法中,我们设置了一个duration成员变量作为每个动画帧的间隔时间,然后在每次调用时累积时间,如果时间超过0.1秒则让分数增加1,如果分数还没达到目标分数则分解他并显示,如果达到了则返回null。   OK!现在工具都创建完了,接下来进入最重要的一个类,首先我们为Constants添加两个常量:
// GUI视口宽度
	public static final float VIEWPORT_GUI_WIDTH = 480f;
		
	// GUI视口高度
	public static final float VIEWPORT_GUI_HEIGHT = 854f;
  看起来该值很怪异,其实不然,因为现在大部分手机屏幕高宽比基本都是16:9,所以我们这里为了不在手机上发生拉伸而设置了该值。接下来创建关键的GameDialog类,该类继承于Stage,在LibGDX中,Stage的重要性不能忽视,但是这里由于篇幅的原因我不打算介绍这些基础内容,如果需要可以在百度上搜索土豆教程,讲的很详细:
package com.art.zok.flappybird.game.UI;

import com.art.zok.flappybird.game.Assets;
import com.art.zok.flappybird.game.WorldController;
import com.art.zok.flappybird.util.Constants;
import com.badlogic.gdx.scenes.scene2d.Action;
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.actions.Actions;
import com.badlogic.gdx.scenes.scene2d.actions.DelayAction;
import com.badlogic.gdx.scenes.scene2d.ui.Button;
import com.badlogic.gdx.scenes.scene2d.ui.Image;
import com.badlogic.gdx.scenes.scene2d.utils.TextureRegionDrawable;
import com.badlogic.gdx.utils.viewport.StretchViewport;

public class GameDialog extends Stage {
	WorldController worldController;
	
	public GameDialog(WorldController worldController) {
		super(new StretchViewport(Constants.VIEWPORT_GUI_WIDTH,
				Constants.VIEWPORT_GUI_HEIGHT));
		this.worldController = worldController;
	}
	
	public void startView() {
		clear();
		// tutorial
		Image tutorial = new Image(Assets.instance.assetUI.tutorial);
		tutorial.setBounds(134, 327, 213, 185);
		// get ready
		Image textReady = new Image(Assets.instance.assetUI.textReady);
		textReady.setBounds(89, 553, 302, 90);
		addActor(tutorial);
		addActor(textReady);
	}
	
	public void gameView() {
		clear();
		// pause 按钮
		Button pause = new Button(new TextureRegionDrawable(Assets.instance.assetUI.buttonPause),
				new TextureRegionDrawable(Assets.instance.assetUI.buttonResume), 
				new TextureRegionDrawable(Assets.instance.assetUI.buttonResume));
		pause.setBounds(410, 784, 50, 50);
		pause.addListener(new InputListener() {
			@Override
			public boolean touchDown(InputEvent event, float x, float y, int pointer, int button) {
			return true;
			}
			@Override
			public void touchUp(InputEvent event, float x, float y, int pointer, int button) {
			worldController.pauseOrResume();
			}
		});
		
		addActor(pause);
				
	}
	
	public void endView() {
		clear();
		
		// 奖牌 
		int medalLevel = (worldController.score < 10)   ? 0 : 
						 (worldController.score < 100)  ? 1 : 
						 (worldController.score < 1000) ? 2 : 3; 
		final Image medal = new Image(Assets.instance.assetUI.medals.get(medalLevel));
		medal.setBounds(93, 375, 75, 72);
		
		// 分数动画
		final ScoreActor scoreActor = new ScoreActor(worldController.score, 393, 430, false);
		
		// 最高分数
		int bestScore = worldController.prefs.getInteger(Constants.BEST_SCORE_KEY); 
		final ScoreActor bestScoreActor = new ScoreActor(bestScore, 393, 350, true);
				
		
		// game over 文本
		Image textGameOver = new Image(Assets.instance.assetUI.textGameOver);
		textGameOver.setBounds(80, 563, 321, 80);
		// actions
		Action textGameAlphaAction = Actions.fadeIn(0.3f);
		Action textGameOverSeqAction = Actions.sequence(Actions.moveTo(80, 580, 0.1f), Actions.moveTo(80, 563, 0.1f));
		Action textGameOverParAction = Actions.parallel(textGameAlphaAction, textGameOverSeqAction);
		textGameOver.addAction(textGameOverParAction);
		
		// score 面板
		Image scorePanel = new Image(Assets.instance.assetUI.scorePanel);
		scorePanel.setBounds(51, -215, 378, 215);
		// actions 
		Action endRunAction = Actions.run(new Runnable() {
			@Override public void run() {
				addActor(medal);
				addActor(scoreActor);
				addActor(bestScoreActor);
			}
		});
		Action scorePanelDelayAction = Actions.delay(0.5f);
		Action scorePanelSeqAction = Actions.sequence(
				scorePanelDelayAction, 
				Actions.moveTo(51, 320, 0.3f),
				endRunAction);
		scorePanel.addAction(scorePanelSeqAction);
		
		// play 按钮
		CustomButton play = new CustomButton(Assets.instance.assetUI.buttonPlay);
		play.setBounds(43, -108, 173, 108);
		// actions 
		Action playDelayAction = Actions.delay(1f);
		Action playSeqAction = Actions.sequence(playDelayAction, Actions.moveTo(43, 175, 0));
		play.addAction(playSeqAction);
		play.addListener(new InputListener() {
			@Override
			public boolean touchDown(InputEvent event, float x, float y, int pointer, int button) {
				return true;
			}
			@Override
			public void touchUp(InputEvent event, float x, float y, int pointer, int button) {
				worldController.restart();
			}
		});
		
		// score 按钮
		CustomButton score = new CustomButton(Assets.instance.assetUI.buttonScore);
		score.setBounds(263, -108, 173, 108);
		// actions 
		DelayAction scoreDelayAction = Actions.delay(1f);
		Action scoreSeqAction = Actions.sequence(scoreDelayAction, Actions.moveTo(263, 175, 0));
		score.addAction(scoreSeqAction);

		addActor(textGameOver);
		addActor(scorePanel);
		addActor(play);
		addActor(score);
		
	}
	
}
  该类我们保存了WorldController的实例句柄,因此GameDialog在WorldController创建史非常合适的。二期我们使用了上述创建的两个常量确定该Stage的视口大小。   该类 最重要的三个方法startView(),gameView()和endView()分别表示初始界面、游戏进行时的界面和游戏结束时的界面。每个方法一开始调用clear()方法清除了所有组件,接下来创建该界面该有的组件和动画类,并添加之。这里其实只涉及几个方法和类,只不过多次调用而已。下面我简单的介绍介个类和方法:
  • Stage:Stage称为舞台类,该类在LibGDX是承载各个组件,如Button、Image等等的工具,该类可以获得输入并分发到各个组件中,重要的一点是,该类是以屏幕左下角为原点的。
  • Image:一个可以显示一张纹理资源的Actor类。
  • Button:包含一张纹理的Actor类,我们可以为该类注册一个监听器,然后当Button被点击时,监听器中对应的代码会被执行。
  • Action:动作类,该类用是所用动作类的父类,我们可以使用动作类制作出旋转、移动、淡入淡出的效果。
  • RunnableAction:该类也是一个动作类,不过该类有些特殊,他不执行什么特殊的动画,他一般被用于在某些动作类执行完后作出一些特定的操作,比如上述代码中,我们在scorePanel完成动作后使用RunnableAction添加奖牌、分数、最高分数组件。
  根据上述分析,我们就很容易理解这里的代码了,首先在startView()我们添加了一个教程纹理图片、一个文本图片;接着在游戏进行中的界面我们清楚前面创建的所有组件,然后添加上暂停/复位按钮;最后在结束界面我们添加多个组件,和组件的动作类,需要注意的是这里使用的DelayAction类和AlphaAction的时间都是精心设计的。   接下来我们修改WorldController类:
...
public class WorldController extends InputAdapter implements Disposable {  
    ...
    public GameDialog dialog;
    public Preferences prefs;
    public FlappyBirdMain main;
    ...
    public WorldController(FlappyBirdMain main) { 
          this.main = main;
         init();
   }
 
    private void init() { 
    	...
    	initDialog();
		InputMultiplexer multiplexer = 
				new InputMultiplexer(dialog, this);
		Gdx.input.setInputProcessor(multiplexer);
    	prefs = Gdx.app.getPreferences(Constants.BEST_SCORE_FILE);  
    }  
    ...
    private void initDialog() {
    	if(dialog == null)
    		dialog = new GameDialog(this);
    	dialog.startView();
    }
    
    ...
    public void pauseOrResume() {
         main.paused = !main.paused;
    }
    
    public void restart() {
          if(isGameOver) 
           init();
    }

    public void update(float deltaTime) {
    	if(!isGameOver) {
    		if(isGameOver = bird.isGameOver()) {
    			saveBestScore();
    			dialog.endView();
    			Gdx.app.debug(TAG, "GAME OVER!");
    		}
    	}
    	...
    }  
    
    @Override
    public boolean touchDown(int screenX, int screenY, int pointer, int button) {
    	if (button == Buttons.LEFT) {
			if(!isStart) {
				isStart = true;
				dialog.gameView();
				bird.beginToSimulate(world);
				land.beginToSimulate(world);
			}
			
			bird.setJumping();
		}
		return true;
    }
    
    @Override
    public void dispose() {
    	world.dispose();
    	dialog.dispose();
    }
}
  首先我们改造了WorldController类的构造函数,因为在前面添加的对话框中我们有个一个暂停按钮,该按钮需要访问主类的paused成员变量,所以我们这里维护一个主类的引用,并且创建了一个pauseOrResume()方法供GameDialog的按钮监听器调用,达到可以暂停/复位效果。还有,我们添加了一个reStart()按钮重新启动游戏,同样该方法也是为GameDialog的按钮监听器调用设计的,注意我们在touchDown中取消了重启游戏的代码。   下面 看看dialog对象,我们在WorldController的整个生命周期维护dialog对象,并且我们打算重用该对象。首先我们在initDialog()初始化他,最后在dispose()中释放他。   这里我们使用了LibGDX的一大特色功能,InputMultiplexer,该类运行我们为LibGDX设定多个输入适配器,设置的每个输入适配器在运行时将根据先后顺序轮询调用,比如我们在屏幕上点击一下,LibGDX首先将点击事件发送给Stage对象,等待Stage处理完成,LibGDX再将点击事件发送给WorldController对象。这里还有一点需要注意,那就是我们可以中断该轮询处理,只要我们在某个事件的处理最后返回true则表明该事件已经完成处理不需要继续给其他输入适配器发生消息了。   观察dialog的三个方法的调用位置,第一个startView()是在初始化函数initDialog()中调用,表明一开始显示初始界面;第二个方法gameView()方法是在touchDown()中调用,也就是游戏刚刚开始的时候显示第二个界面,也就是暂停/复位按钮;第三个方法endView()是在update()中调用,当我们确定游戏已经结束,就显示结束画面告知我们最后的信息。   最后,修改WorldRenderer类显示dialog对象:
public class WorldRenderer implements Disposable {  
	
    ...
    // 游戏GUI渲染方法
   private void renderGui(SpriteBatch batch) {
         if(!worldController.isGameOver) {
                batch.setProjectionMatrix(camera.combined);
                batch.begin();
                renderScore(batch);
                batch.end();
          }
          worldController.dialog.act();
          worldController.dialog.draw();
   }
}
  我们修改了renderGui()方法,在该方法我们判断游戏是否结束,如果结束在不会只分数。接着我们渲染了worldController中的dialog对象。   最 后我们还必须修改FlappyBirdMain类:
public class FlappyBirdMain implements ApplicationListener {  
    ...
    public boolean paused;
    
    @Override 
    public void create() { 
    	...
    	worldController = new WorldController(this);
    	worldRenderer = new WorldRenderer(worldController);
    	
    	paused = false;
    }
  我们经paused对象修改为公有的访问权限,一边worldController中可以顺利访问并修改,最后在create()方法中,我们必须为WorldController()的构造函数传入this参数。
  说了这么多,该是测试的时候了: LibGDX重建Flappy Bird——(7) 添加GUI信息LibGDX重建Flappy Bird——(7) 添加GUI信息LibGDX重建Flappy Bird——(7) 添加GUI信息   很高兴,我们三个界面都完美的显示了。本章内容到此结束,下章将为应用添加声音资源和启动画面等。   由于时间较为紧迫,写的比较粗糙,文中难免有所疏漏,望读者见谅,如果什么问题,可以尽管提问,我会及时解答,如果你有更佳的实现技术,请不吝赐教!
















转载于:https://my.oschina.net/u/2432369/blog/610407

上一篇:LibGDX重建Flappy Bird——(5) 添加Box2D物理仿真和游戏逻辑


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