Học làm game (7) - Lập trình với Unity (Hoàn thành game Tetris)

Bài trước: Học làm game (6) - Lập trình với Unity (LT chức năng)

-----

Tạo lớp Group

Chúng ta cùng viết các đoạn mã còn lại cho game Tetris. Viết mã cho lớp Group. Viết riêng vào tập tin Group.cs.

Trong giao diện Unity > vào mục Project > Create > Script C# > đặt tên là Group.

Lớp Group gồm các đoạn mã xử lý liên quan đến khối hình (group), ví dụ các GroupI, GroupO,…v.v.

Viết hàm kiểm tra một ô vuông trong một Khối hình là hợp lệ

Như đã biết, mỗi một GameObject đều có một thành phần đi kèm là transform, nó chứa thông tin về vị trí (position), xoay hình (rotation), co dãn (scale).

Một Khối hình (Group) là một GameObject, được tạo bởi 4 ô vuông (block). Mỗi ô vuông cũng là một GameObject, nên cũng có 4 thành phần transform tương ứng.

Đoạn mã sau sẽ kiểm tra tính hợp lệ của một ô vuông, bằng cách: duyệt từng ô vuông trong một Khối hình để đảm bảo ô vuông đó nằm trong Khung hình chữ nhật và không chồng lấn (intersection) lên ô vuông của Khối hình khác.

 

    // Kiểm tra một ô vuông trong Group là hợp lệ hay không?

    bool isValidGridPos()

    {

        foreach (Transform child in transform)

        {

            Vector2 v = Playfield.roundVec2(child.position);

            // không ở trong Khung chữ nhật?

            if (!Playfield.insideBorder(v))

                return false;

            // Vẫn ở trong Khung chữ nhật, nhưng nằm đè lên Ô vuông của Group khác

            if (Playfield.grid[(int)v.x, (int)v.y] != null &&

                Playfield.grid[(int)v.x, (int)v.y].parent != transform)

                return false;

        }

        return true;

    }

Viết hàm cập nhật lại Khung lưới (grid) khi một Khối hình thay đổi vị trí

Khi một Khối hình thay đổi vị trí, chúng ta sẽ duyệt qua toàn bộ các ô trong Khung lưới, tìm và xóa các Ô vuông thuộc cùng một Khối hình đang xét (dựa vào thuộc tính parent). Sau đó, thêm các Ô vuông của Khối hình hiện tại (transform) vào Khung lưới.

    // cập nhật lại Khung lưới (grid) khi một Khối hình thay đổi vị trí

    void updateGrid()

    {

        // xóa các ô vuông của một Khối hình ra khỏi Khung lưới

        for (int y = 0; y < Playfield.h; ++y)

            for (int x = 0; x < Playfield.w; ++x)

                if (Playfield.grid[x, y] != null)

                    if (Playfield.grid[x, y].parent == transform)

                        Playfield.grid[x, y] = null;

        // thêm các ô vuông của Khối hình đã bị thay đổi vị trí vào Khung lưới

        foreach (Transform child in transform)

        {

            Vector2 v = Playfield.roundVec2(child.position);

            Playfield.grid[(int)v.x, (int)v.y] = child;

        }

    }

Đi ngang và rớt xuống

Chúng ta sẽ cùng lập trình để xử lý khi người dùng bấm phím mũi tên sang trái, sang phải, đi xuống.

Chúng ta viết mã nguồn trong hàm void Update(), lớp Group.

    // Update is called once per frame

    void Update()

    {

        // Đi sang trái

        if (Input.GetKeyDown(KeyCode.LeftArrow))

        {

            // thay đổi vị trí của Khối hình

            transform.position += new Vector3(-1, 0, 0);

            // kiểm tra tính hợp lệ của Khối hình

            if (isValidGridPos())

                // Nếu hợp lệ, cập nhật Khung chữ nhật (grid)

                updateGrid();

            else

                // nếu Khối hình không hợp lệ, hoàn lại vị trí cũ

                transform.position += new Vector3(1, 0, 0);

        }

        // Đi sang phải

        else if (Input.GetKeyDown(KeyCode.RightArrow))

        {

            // thay đổi vị trí của Khối hình

            transform.position += new Vector3(1, 0, 0);

            // kiểm tra tính hợp lệ của Khối hình

            if (isValidGridPos())

                // Nếu hợp lệ, cập nhật Khung chữ nhật (grid)

                updateGrid();

            else

                // nếu Khối hình không hợp lệ, hoàn lại vị trí cũ

                transform.position += new Vector3(-1, 0, 0);

        }

        // Xoay Khối hình

        else if (Input.GetKeyDown(KeyCode.UpArrow))

        {

            transform.Rotate(0, 0, -90);

            // kiểm tra tính hợp lệ của Khối hình

            if (isValidGridPos())

                // Nếu hợp lệ, cập nhật Khung chữ nhật (grid)

                updateGrid();

            else

                // nếu Khối hình không hợp lệ, hoàn lại vị trí cũ

                transform.Rotate(0, 0, 90);

        }

        // Rơi xuống

        else if (Input.GetKeyDown(KeyCode.DownArrow))

        {

            // thay đổi vị trí của Khối hình

            transform.position += new Vector3(0, -1, 0);

            // kiểm tra tính hợp lệ của Khối hình

            if (isValidGridPos())

                // Nếu hợp lệ, cập nhật Khung chữ nhật (grid)

                updateGrid();

            else

                // nếu Khối hình không hợp lệ, hoàn lại vị trí cũ

                transform.position += new Vector3(0, 1, 0);

            // xóa hàng ngang đã được lấp đầy ô vuông

            Playfield.deleteFullRows();

            // sinh ra Khối hình kế tiếp

            FindObjectOfType<Spawner>().spawnNext();

            // vô hiệu đoạn mã

            enabled = false;

        }

    }

Chúng ta có thể thiết lập cho các Khối hình rơi tự động, mỗi giây rơi một mức, như sau:

        // biến thời gian

        float lastFall = 0;

Thay đổi mã nguồn một chút,

        // Rơi xuống

        else if (Input.GetKeyDown(KeyCode.DownArrow) || Time.time - lastFall >= 1)

        {

            // thay đổi vị trí của Khối hình

            transform.position += new Vector3(0, -1, 0);

            // kiểm tra tính hợp lệ của Khối hình

            if (isValidGridPos())

                // Nếu hợp lệ, cập nhật Khung chữ nhật (grid)

                updateGrid();

            else

                // nếu Khối hình không hợp lệ, hoàn lại vị trí cũ

                transform.position += new Vector3(0, 1, 0);

            // xóa hàng ngang đã được lấp đầy ô vuông

            Playfield.deleteFullRows();

            // sinh ra Khối hình kế tiếp

            FindObjectOfType<Spawner>().spawnNext();

            // vô hiệu đoạn mã

            enabled = false;

        }

        lastFall = Time.time;

    }

Đoạn mã xử lý cho trường hợp kết thúc game (gameover): khi một Khối hình mới tạo ra đã đụng phải Khối hình khác, sẽ kết thúc game.

    // Start is called before the first frame update

    void Start()

    {

        // Khối hình mới sinh ra đã đụng phải khối hình khác > gameover

        if (!isValidGridPos())

        {

            Debug.Log("Game over");

            Destroy(gameObject);

        }

       

    }

Việc viết mã nguồn đã hoàn thành.

Với mỗi prefab mà chúng ta đã tạo trong Project Area, bấm vào prefab, sau đó bấm vào nút Open Prefab trong Inspector. Xem hình minh họa.



Sau đó, bấm vào nút Add Component


Trong mục Component, tìm tới mục Script > chọn đoạn mã Group.


Bạn sẽ thấy đoạn mã Group đã được thêm vào prefab.


Làm tương tự cho các Group còn lại: GroupO, GroupL, …v.v.

Chúc mừng bạn đã làm xong game.

Chúng ta cùng bấm play để chơi game Tetris.

Bị lỗi:

– Lúc bắt đầu chơi, khi không bấm phím nào. Khối hình chỉ rơi một mức rồi dừng. Các bạn thử sửa mã nguồn xem sao.

-----

Cập nhật: 7/4/2023