Game với nhiều màn hình

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à GameScreen.

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à GameSreen để 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 GameScreen 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 GameScreen.

Đầ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, GameScreenEndScreen.

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

Your email address will not be published. Required fields are marked *