Khi chơi một trò chơi thì thông thường ta sẽ thấy có nhiều hơn một màn hình. Ví dụ như là: Màn hình cài đặt trò chơi, màn hình chơi chính, màn hình kết thúc trò chơi,… Trong bài này chúng ta sẽ cùng tìm hiểu cách để thao tác hiển thị nhiều màn hình trong một trò chơi. Hai classes chính mà ta sẽ tìm hiểu là Game và Screen.
Giả sử chúng ta cần làm một trò chơi mà sẽ có ba màn hình: màn hình vào game, màn hình chơi và màn hình kết thúc trò chơi.
Theo yêu cầu trên ta có thể sẽ nghĩ luôn ra một phương án đó là ta sẽ tạo ra một biến mà chứa thông tin để ta biết là mình đang ở màn hình nào. Và sẽ dựa vào giá trị của biến đó để vẽ ra màn hình tương ứng trong hàm render().
Kết quả code thu được sau khi thực hiện theo cách trên:
public class SimpleGameActivity extends AndroidApplication { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); AndroidApplicationConfiguration cfg = new AndroidApplicationConfiguration(); cfg.r = cfg.g = cfg.b = cfg.a - 8; setContentView(initializeForView(new SimpleGame())); } static class SimpleGame extends ApplicationAdapter { enum Screen { TITLE, MAIN_GAME, GAME_OVER; } Screen currentScreen = Screen.TITLE; SpriteBatch batch; ShapeRenderer shapeRenderer; BitmapFont font; float circleX = 300; float circleY = 150; float circleRadius = 50; float xSpeed = 4; float ySpeed = 3; private final String playTapMessage = "Tap to play"; Rectangle playTextFrame; float playTextX = 0; float playTextY = 0; private final String restartTabMessage = "Tap to restart."; Rectangle restartTextFrame; float restartTextX = 0; float restartTextY = 0; @Override public void create() { batch = new SpriteBatch(); shapeRenderer = new ShapeRenderer(); font = new BitmapFont(); font.getData().setScale(5); playTextX = Gdx.graphics.getWidth() * .25f; playTextY = Gdx.graphics.getHeight() * .25f; GlyphLayout playFrameLayout = new GlyphLayout(); playFrameLayout.setText(font, playTapMessage); playTextFrame = new Rectangle(playTextX, Gdx.graphics.getHeight() - (playTextY - playFrameLayout.height), playFrameLayout.width, playFrameLayout.height); restartTextX = Gdx.graphics.getWidth() * .25f; restartTextY = Gdx.graphics.getHeight() * .25f; GlyphLayout restartLayout = new GlyphLayout(); restartLayout.setText(font, restartTabMessage); restartTextFrame = new Rectangle(restartTextX, Gdx.graphics.getHeight() - (restartTextY - restartLayout.height), restartLayout.width, restartLayout.height); Gdx.input.setInputProcessor(new InputAdapter() { @Override public boolean touchDown(int x, int y, int pointer, int button) { if (currentScreen == Screen.MAIN_GAME) { int renderY = Gdx.graphics.getHeight() - y; if (Vector2.dst(circleX, circleY, x, renderY) < circleRadius) { currentScreen = Screen.GAME_OVER; } } else if (currentScreen == Screen.TITLE) { if (playTextFrame.contains(x, y)) { currentScreen = Screen.MAIN_GAME; } } else if (currentScreen == Screen.GAME_OVER) { if (restartTextFrame.contains(x, y)) { currentScreen = Screen.TITLE; } } return true; } }); } @Override public void render() { if (currentScreen == Screen.TITLE) { Gdx.gl.glClearColor(0, 0, 0, 1); Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT); batch.begin(); font.draw(batch, "Title Screen!", Gdx.graphics.getWidth() * .25f, Gdx.graphics.getHeight() * .75f); font.draw(batch, "Tap the circle to win.", Gdx.graphics.getWidth() * .25f, Gdx.graphics.getHeight() * .5f); font.draw(batch, playTapMessage, playTextX, playTextY); batch.end(); } else if (currentScreen == Screen.MAIN_GAME) { circleX += xSpeed; circleY += ySpeed; if (circleX < 0 || circleX > Gdx.graphics.getWidth()) { xSpeed *= -1; } if (circleY < 0 || circleY > Gdx.graphics.getHeight()) { ySpeed *= -1; } Gdx.gl.glClearColor(0, 0, .25f, 1); Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT); shapeRenderer.begin(ShapeRenderer.ShapeType.Filled); shapeRenderer.setColor(Color.RED); shapeRenderer.circle(circleX, circleY, 75); shapeRenderer.end(); } else if (currentScreen == Screen.GAME_OVER) { Gdx.gl.glClearColor(0, 0, 0, 1); Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT); batch.begin(); font.draw(batch, "You win!", Gdx.graphics.getWidth() * .25f, Gdx.graphics.getHeight() * .75f); font.draw(batch, restartTabMessage, restartTextX, restartTextY); batch.end(); } } @Override public void dispose() { shapeRenderer.dispose(); } } }
Cách trên chúng ta sử dụng một enum để chứa trạng thái của màn hình tương ứng (TITLE, MAIN_GAME, GAME_OVER). Việc chuyển đổi màn hình sẽ dựa vào việc bắt các thao tác tương ứng của người chơi được xử lý trong hàm touchDown. Hàm render() sẽ dựa vào trạng thái của biến currentScreen để vẽ ra màn hình game tương ứng.
Chạy game lên sẽ như sau:

Với một game đơn giản như trên thì cách tiếp cận này có thể chấp nhận được. Tuy nhiên khi mà trò chơi trở nên phức tạp hơn, các màn chơi sẽ phát sinh ra thêm nhiều chức năng, nhiều thứ phải xử lý hơn nữa thì cách trên sẽ không còn phù hợp nữa.
Giờ ta hãy cùng tìm hiểu cách xử lý vấn đề mà libGDX hướng chúng ta nên đi theo.
libGDX cung cấp cho ta 2 class đó là Game và Sreen để hỗ trợ việc đóng gói các màn hình trò chơi và làm cho việc phát triển các game phức tạp nhiều màn hình trở nên dễ dàng hơn.
Khi app dụng class Game và Screen vào project thì trò chơi của ta sẽ có một class kế thừa class Game và các class màn hình trò chơi sẽ implement class Screen.

Game
Class Game được kế thừa từ class ApplicationAdapter và trong một project sẽ thường chỉ có một class mà được kế thừa từ Game. Class kế thừa này sẽ chứa các tài nguyên được chia sẻ trong cả trò chơi. Nó sẽ không chứa bất kỳ logic hay xử lý hiện thị của bất kỳ màn hình trò chơi nào.
Screen
Là một interface chứa các hàm vòng đời của chính nó mà được gọi bởi libGDX, cho phép chúng ta tách biệt logic của các màn hình game. Trong một project thường sẽ có nhiều class mà được implements Screen. Mỗi class này sẽ đóng vai trò là một màn hình game của chúng ta.
Giờ chúng ta sẽ cùng sửa lại trò chơi trên theo cấu trúc project mới sử dụng Game và Screen.
Đầu tiên ta sẽ tạo một class là BallGame kế thừa class Game. Trong hàm create() ta sử dụng setScreen() để chuyển tới màn hình mà ta mong muốn, ở đây là màn hình TitleScreen()
public class BallGame extends Game { public SpriteBatch batch; public ShapeRenderer shapeRenderer; public BitmapFont font; @Override public void create() { batch = new SpriteBatch(); shapeRenderer = new ShapeRenderer(); font = new BitmapFont(); font.getData().setScale(5); setScreen(new TitleScreen(this)); } @Override public void dispose() { super.dispose(); batch.dispose(); shapeRenderer.dispose(); font.dispose(); } }
Tiếp theo chúng ta sẽ tách code của từng màn hình ra các class riêng biệt. Ta sẽ có 3 class tương ứng với 3 màn hình là: TitleScreen, GameScreen và EndScreen.
TitleScreen
public class TitleScreen extends ScreenAdapter { BallGame game; private final String playTapMessage = "Tap to play"; Rectangle playTextFrame; float playTextX = 0; float playTextY = 0; public TitleScreen(BallGame game) { this.game = game; playTextX = Gdx.graphics.getWidth() * .25f; playTextY = Gdx.graphics.getHeight() * .25f; GlyphLayout playFrameLayout = new GlyphLayout(); playFrameLayout.setText(this.game.font, playTapMessage); playTextFrame = new Rectangle(playTextX, Gdx.graphics.getHeight() - (playTextY - playFrameLayout.height), playFrameLayout.width, playFrameLayout.height); } @Override public void show(){ Gdx.input.setInputProcessor(new InputAdapter() { @Override public boolean touchDown(int screenX, int screenY, int pointer, int button) { if (playTextFrame.contains(screenX, screenY)) { game.setScreen(new GameScreen(game)); } return true; } }); } @Override public void render(float delta) { Gdx.gl.glClearColor(0, 0, 0, 1); Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT); game.batch.begin(); game.font.draw(game.batch, "Title Screen!", Gdx.graphics.getWidth() * .25f, Gdx.graphics.getHeight() * .75f); game.font.draw(game.batch, "Click the circle to win.", Gdx.graphics.getWidth() * .25f, Gdx.graphics.getHeight() * .5f); game.font.draw(game.batch, playTapMessage, playTextX, playTextY); game.batch.end(); } @Override public void hide(){ Gdx.input.setInputProcessor(null); } }
Class TitleScreen kế thừa class ScreenAdapter (ScreenAdapter là một class được implement từ Screen, khi ta kế thừa ScreenAdapter thì ta sẽ không phải viết code triển khai các function trong Screen interface mà ta không cần dùng đến)
Hàm show() sẽ được tự động gọi khi Screen trở thành màn hình hiện tại của trò chơi. Như ở trên nó sẽ thiết lập các xử lý liên quan đến thao tác của người chơi.
Hàm render() sẽ được gọi nhiều lần (thường là 60 frames trên giây – 60 fps)
Hàm hide() được gọi khi màn hình chơi không còn là màn hình hiện tại của game. Trong class TitleScreen hàm hide() sẽ xoá việc xử lý thao tác của người chơi.
GameScreen
public class GameScreen extends ScreenAdapter { BallGame game; float circleX = 300; float circleY = 150; float circleRadius = 50; float xSpeed = 4; float ySpeed = 3; public GameScreen(BallGame game) { this.game = game; } @Override public void show() { Gdx.input.setInputProcessor(new InputAdapter() { @Override public boolean touchDown(int x, int y, int pointer, int button) { int renderY = Gdx.graphics.getHeight() - y; if (Vector2.dst(circleX, circleY, x, renderY) < circleRadius) { game.setScreen(new EndScreen(game)); } return true; } }); } @Override public void render(float delta) { circleX += xSpeed; circleY += ySpeed; if (circleX < 0 || circleX > Gdx.graphics.getWidth()) { xSpeed *= -1; } if (circleY < 0 || circleY > Gdx.graphics.getHeight()) { ySpeed *= -1; } Gdx.gl.glClearColor(0, 0, 0f, 1); Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT); game.shapeRenderer.begin(ShapeRenderer.ShapeType.Filled); game.shapeRenderer.setColor(Color.RED); game.shapeRenderer.circle(circleX, circleY, 75); game.shapeRenderer.end(); } @Override public void hide() { Gdx.input.setInputProcessor(null); } }
EndScreen
public class EndScreen extends ScreenAdapter { BallGame game; private final String restartTabMessage = "Tap to restart."; Rectangle restartTextFrame; float restartTextX = 0; float restartTextY = 0; public EndScreen(BallGame game) { this.game = game; restartTextX = Gdx.graphics.getWidth() * .25f; restartTextY = Gdx.graphics.getHeight() * .25f; GlyphLayout restartLayout = new GlyphLayout(); restartLayout.setText(this.game.font, restartTabMessage); restartTextFrame = new Rectangle(restartTextX, Gdx.graphics.getHeight() - (restartTextY - restartLayout.height), restartLayout.width, restartLayout.height); } @Override public void show() { Gdx.input.setInputProcessor(new InputAdapter() { @Override public boolean touchDown(int screenX, int screenY, int pointer, int button) { if (restartTextFrame.contains(screenX, screenY)) { game.setScreen(new TitleScreen(game)); } return true; } }); } @Override public void render(float delta) { Gdx.gl.glClearColor(0, 0, 0, 1); Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT); game.batch.begin(); game.font.draw(game.batch, "You win!", Gdx.graphics.getWidth() * .25f, Gdx.graphics.getHeight() * .75f); game.font.draw(game.batch, restartTabMessage, restartTextX, restartTextY); game.batch.end(); } @Override public void hide() { Gdx.input.setInputProcessor(null); } }
Toàn bộ code của phần này ta có thể tham khảo tại đây
Leave a Reply