LibGDX Rebuild Flappy Bird - Add GUI Information

Original Link: https://my.oschina.net/u/2432369/blog/610407

Source link for this chapter: http://pan.baidu.com/s/1hruBkgc Password: 94iq

The previous chapter shows that we have basically finished the game logic of FlappyBird. Next, we will add some GUI information, such as scores, buttons, etc.

Score GUI

First we need to maintain an int value for the WorldController to represent the score the current player has earned.Modify the WorldController and add the appropriate methods:

...
public class WorldController extends InputAdapter implements Disposable {  
    ...
    public int score;
    ...
    private void init() { 
    	...
    	score = 0;
    	isStart = false;
    	...
    }  
    ...
    // Calculate score
	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();
    }  
...
}

First, we added an integer member variable, score, and initialized init() to zero.Next, a calculateScore() method is added to calculate the current score, which traverses all Pipe objects and determines if the x-coordinate of the pipe object is less than the x-coordinate of the Bird object, and if it is less, gets the current pipe score and prints debugging information.Finally, we call this method in update() to calculate the current score.
Next we add a GUI of the score to the game, which is the current score displayed on the top of the screen.First add two methods to WorldRenderer:
// Game GUI Rendering Method
 	private void renderGui(SpriteBatch batch) {
 		batch.setProjectionMatrix(camera.combined);
 		batch.begin();
 		renderScore(batch);
 		batch.end();
 	}
 	
 	// Fraction
 	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;
 		}
 	}
   A new class, Tools.splitInteger(int), is used in the above method, which is a static method of a class we have just created, which splits bits of an integer apart.Next, we create the class and its corresponding methods:
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;
	}
}
The above methods are easy to add, so share them if you have better algorithms or techniques.Now we can test the application:
Next, add a permanent save method with the highest score for Bird.First add two constants to the Constants class:
// best score file
	public static final String BEST_SCORE_FILE = "BestScoreFile";
	// best score key
	public static final String BEST_SCORE_KEY = "best_score_key";
The first constant represents the name of the file saved with the highest score, and the second constant represents the key value that holds the highest score.Libgdx provides a very convenient class for us to permanently save data. If we want to save a data, we just need to provide a key value of a string constant for that value. It works like a hash table. Let's see the code below:
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!");
    		}
    	}
    	...
    }  
    
}
First we create a Preferences class member variable, then we get a Preferences object in the initialization method init() through the Gdx.app module, where we need a permanent save As a parameter, we use the constant BEST_SCORE_FILE.Next we added a new method, saveBestScore(), to save the highest score, and in update(), we decided to save the highest score if the game ended.
Next, we will create a GUI dialog box that contains the start and end interfaces with pause buttons in the game. The start interface contains a tutorial picture and a title picture. The end interface contains a closing picture and a score panel, which contains the scores, medals, etc.And the maximum score, the dialog box also contains two buttons to restart the game and so on.
First let me create a custom button class CustomButton, which inherits the Button class, which implements a button with only one picture and will shift the picture down a certain distance when the button is pressed:
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);
		}
	}
}
You can see that this class is simple, and the most critical thing is to determine the two states in the render() method, offset the drawing if the button is pressed, and use the default drawing if it is not.
Under Face We create an animated Actor for displaying fractions. There are two situations for this Actor, one is to display fractions statically, the other is to animate from 0 until the target fraction is reached:
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);
		}
		
	}
}
The constructor for this class contains four parameters: the first parameter is the target score to be realistic, the second and third parameters represent the coordinates to be displayed, and the fourth parameter represents the coordinates to be displayed dynamically through static display.The constructor first saves the parameters passed in, then gets a list of digital texture resources, and finally uses the previously added Tools class method to split the target fraction into arrays.
Next Let's first look at the draw() method. If it is a static display, the update() method is not called to draw the texture group of the target score directly.If it's in the form of animation, we need to call the update() method when rendering each frame. In the update() method, we set a duration member variable as the interval between each animation frame, and then accumulate the time at each call. If the time exceeds 0.1 seconds, increase the fraction by 1, if the fraction has not reached the goalThe scalar fraction decomposes it and displays it, and returns null if it is reached.
OK!Now that the tools are all created, let's move on to the most important class, starting with two constants for Constants:
// GUI Viewport Width
	public static final float VIEWPORT_GUI_WIDTH = 480f;
		
	// GUI Viewport Height
	public static final float VIEWPORT_GUI_HEIGHT = 854f;
This value seems strange, but it's not because most mobile screens are now 16:9 aspect ratio, so we set this value here to avoid stretching on mobile phones.Next, create the key GameDialog class, which inherits from Stage. Stage is important in LibGDX, but I don't want to cover these basics here because of the space. If you need to search for potato tutorials in Baidu, you can go into more detail:
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
		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();
		
		// Medal 
		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);
		
		// Score Animation
		final ScoreActor scoreActor = new ScoreActor(worldController.score, 393, 430, false);
		
		// Maximum score
		int bestScore = worldController.prefs.getInteger(Constants.BEST_SCORE_KEY); 
		final ScoreActor bestScoreActor = new ScoreActor(bestScore, 393, 350, true);
				
		
		// game over text
		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 panel
		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 button
		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 button
		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);
		
	}
	
}
This class holds the instance handle of the WorldController, so GameDialog is a good place to create a history in the WorldController.In Phase 2, we used the two constants created above to determine the port size of the Stage.
This class The three most important methods, startView(), gameView(), and endView(), represent the initial interface, the interface as the game progresses, and the interface at the end of the game, respectively.Each method initially calls the clear() method to clean up all components, then creates and adds all the components and animation classes for the interface.There are actually only a few methods and classes involved, just a few calls.Here's a brief introduction to the classes and methods:
  • Stage: Stage is known as the stage class. LibGDX is a tool that hosts components such as Button, Image, and so on. This class can get input and distribute it to components. Importantly, this class is based on the lower left corner of the screen.
  • Image: An Actor class that can display a texture resource.
  • Button: An Actor class that contains a texture for which we can register a listener, and when Button is clicked, the corresponding code in the listener is executed.
  • Action: The action class, which is the parent class of the action class used, can be used to create effects such as rotation, movement, fading in and out.
  • RunnableAction: This class is also an action class, but it has some special features. It doesn't perform any special animations. It is usually used to do some specific actions after the execution of certain action classes. For example, in the code above, we use RunnableAction to add medals, scores, and highest scores after the action is completed by scorePanel Several components.
From the above analysis, we can easily understand the code here. First we added a tutorial texture picture, a text picture in startView(); then in the running interface we know all the components we created before, then we added the pause/reset button; and finally in the ending interface we addedMultiple components, and their action classes, it is important to note that the DelayAction class and the AlphaAction time used here are carefully designed.
Next we modify the WorldController class:
...
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();
    }
}
First, we modified the constructor of the WorldController class because we had a pause button in the dialog box we added earlier that needed access to the paused member variable of the main class, so we maintained a reference to the main class and created a pauseOrResume() method for GameDialog's buttons to listen onCalled by the server to achieve a pause/reset effect.Also, we added a reStart() button to restart the game, which was also designed for GameDialog's button listener call, noting that we canceled the code to restart the game in touchDown.
Below Looking at the dialog object, we maintain it throughout the life cycle of WorldController and we intend to reuse it.First we initialize initDialog() and then release him in dispose().
Here we use a feature of LibGDX, InputMultiplexer, which runs by setting up multiple input adapters for LibGDX. Each input adapter set will poll for calls in sequence at runtime. For example, if we click on the screen, LibGDX first sends the click event to the Stage object.Waiting for the Stage processing to complete, LibGDX sends the click event to the WorldController object.One more thing to note here is that we can interrupt this polling process, as long as we return true at the end of an event's processing, it means that the event has finished processing and no further messages need to be sent to other input adapters.
Observe where the three dialog methods are called, the first startView() is called in the initDialog() initialization function to indicate that the initial interface is displayed at the beginning; the second method, gameView(), is called in touchDown(), which is to show the second interface at the beginning of the game, namely the pause/reset buttonThe third method, endView(), is called in update(), and when we are sure the game is over, the end screen shows us the last information.
Finally, modify the WorldRenderer class to display dialog objects:
public class WorldRenderer implements Disposable {  
	
    ...
    // Game GUI Rendering Method
   private void renderGui(SpriteBatch batch) {
         if(!worldController.isGameOver) {
                batch.setProjectionMatrix(camera.combined);
                batch.begin();
                renderScore(batch);
                batch.end();
          }
          worldController.dialog.act();
          worldController.dialog.draw();
   }
}
We modified the renderGui() method, in which we judged whether the game ended or not, and if it ended, it would not only score.Next we rendered the dialog object in the worldController.
Most Then we have to modify the FlappyBirdMain class:
public class FlappyBirdMain implements ApplicationListener {  
    ...
    public boolean paused;
    
    @Override 
    public void create() { 
    	...
    	worldController = new WorldController(this);
    	worldRenderer = new WorldRenderer(worldController);
    	
    	paused = false;
    }
We modify the paused object to public access, which can be accessed and modified smoothly in the worldController. Finally, in the create() method, we have to pass in the this parameter for the constructor of WorldController().
With all that said, it's time to test:
I'm glad that all three of our interfaces are perfectly displayed.This concludes the chapter, which adds sound resources and splash screens to the application.
Due to the urgency of time and the coarseness of writing, it is unavoidable that there are omissions in this article. Readers will be forgiven. If there are any questions you can ask, I will answer them in time. If you have better implementation technology, please don't hesitate to give them advice!

















Reproduced at: https://my.oschina.net/u/2432369/blog/610407

Tags: less Mobile

Posted on Tue, 10 Sep 2019 14:33:28 -0700 by remlabm