Làm game Flappy kiểu "bay qua ống" trong Unity: cơ chế vỗ cánh, ống di chuyển và tính điểm

  • author-image

    Nguyễn Đình Cường

  • blog-tag
  • blog-comment 0 comment
  • created-date 09 Nov, 2026
blog-thumbnail

Game casual một chạm như "chim bay qua ống" trông đơn giản nhưng gói gọn nhiều ý tưởng nền tảng: lực vật lý, sinh đối tượng liên tục, tính điểm và kết thúc ván. Bài này hướng dẫn bạn làm một game tương tự trong Unity 2D, dùng hình khối tự vẽ, không cần tài nguyên ngoài.

Thiết kế ý tưởng

Chú chim chịu trọng lực, mỗi lần bấm phím hoặc chạm màn hình thì được đẩy lên. Các cặp ống xanh trượt từ phải sang trái với khe hở ở giữa nằm ở độ cao ngẫu nhiên. Chạm ống hoặc rơi xuống đất thì thua. Mỗi cặp ống vượt qua được cộng một điểm. Chú ý, điều ta di chuyển là ống chứ không phải chú chim theo chiều ngang, nhờ đó thế giới không cần mở rộng vô tận.

Bước 1: Chú chim

  1. Tạo project 2D. Tạo Sprite hình tròn tên Bird, thêm Rigidbody 2D và Circle Collider 2D.
  2. Đặt Tag là Player. Đặt camera dạng Orthographic (mặc định ở mẫu 2D).
  3. Gắn script sau:
using UnityEngine;

public class Bird : MonoBehaviour
{
    [SerializeField] private float flapForce = 6f;

    private Rigidbody2D rb;
    private bool isDead;

    void Awake()
    {
        rb = GetComponent<Rigidbody2D>();
    }

    void Update()
    {
        if (isDead) return;

        if (Input.GetKeyDown(KeyCode.Space) || Input.GetMouseButtonDown(0))
        {
            rb.velocity = new Vector2(0f, flapForce);
        }
    }

    void OnCollisionEnter2D(Collision2D collision)
    {
        if (isDead) return;
        isDead = true;
        GameManager.Instance.GameOver();
    }
}

Cách đặt trực tiếp vận tốc dọc thay vì cộng lực giúp cú vỗ cánh luôn cao như nhau, tạo cảm giác ổn định. Hàm GetMouseButtonDown(0) cũng nhận cú chạm đầu tiên trên điện thoại. Nếu phiên bản Unity của bạn đổi tên velocity thành linearVelocity, hãy làm theo gợi ý của trình biên dịch.

Bước 2: Cặp ống di chuyển

Tạo một GameObject trống PipePair chứa hai Sprite hình chữ nhật xanh là ống trên và ống dưới, cách nhau một khe, mỗi ống có Box Collider 2D. Thêm một đối tượng con thứ ba ScoreZone nằm trong khe, có Box Collider 2D bật Is Trigger, dùng để phát hiện chim vượt qua. Kéo PipePair vào thư mục Project tạo prefab rồi xóa khỏi scene. Gắn script di chuyển:

using UnityEngine;

public class PipeMover : MonoBehaviour
{
    [SerializeField] private float speed = 2.5f;
    [SerializeField] private float destroyX = -12f;

    void Update()
    {
        transform.Translate(Vector2.left * speed * Time.deltaTime);

        if (transform.position.x < destroyX)
        {
            Destroy(gameObject);
        }
    }
}

Việc hủy ống khi ra khỏi màn hình rất quan trọng, nếu không số đối tượng sẽ tăng mãi và làm game chậm dần.

Bước 3: Bộ sinh ống

using System.Collections;
using UnityEngine;

public class PipeSpawner : MonoBehaviour
{
    [SerializeField] private GameObject pipePrefab;
    [SerializeField] private float interval = 1.8f;
    [SerializeField] private float minY = -2f;
    [SerializeField] private float maxY = 2f;
    [SerializeField] private float spawnX = 10f;

    void Start()
    {
        StartCoroutine(SpawnLoop());
    }

    private IEnumerator SpawnLoop()
    {
        while (!GameManager.Instance.IsGameOver)
        {
            float y = Random.Range(minY, maxY);
            Instantiate(pipePrefab, new Vector3(spawnX, y, 0f), Quaternion.identity);
            yield return new WaitForSeconds(interval);
        }
    }
}

Coroutine cho phép "chờ" mà không chặn game. Mỗi vòng lặp sinh một cặp ống ở độ cao ngẫu nhiên rồi nghỉ interval giây. Gắn script này vào một GameObject trống và kéo prefab vào ô tương ứng.

Bước 4: Tính điểm và kết thúc ván

using UnityEngine;
using UnityEngine.SceneManagement;
using TMPro;

public class GameManager : MonoBehaviour
{
    public static GameManager Instance { get; private set; }

    [SerializeField] private TextMeshProUGUI scoreText;
    [SerializeField] private GameObject gameOverPanel;

    public bool IsGameOver { get; private set; }
    private int score;

    void Awake()
    {
        Instance = this;
        Time.timeScale = 1f;
    }

    public void AddScore()
    {
        if (IsGameOver) return;
        score++;
        scoreText.text = score.ToString();
    }

    public void GameOver()
    {
        IsGameOver = true;
        gameOverPanel.SetActive(true);
        Time.timeScale = 0f;
    }

    public void Restart()
    {
        SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex);
    }
}

Cho đối tượng ScoreZone một script nhỏ: trong OnTriggerEnter2D, nếu đối tượng đi vào có Tag Player thì gọi GameManager.Instance.AddScore(). Tạo nút "Chơi lại" trong panel Game Over gọi hàm Restart. Đặt nền đất bằng một Sprite dài có Box Collider 2D để chim thua khi rơi xuống.

Chỉnh độ khó và cảm giác chơi

  • Khe hở quá nhỏ hay trọng lực quá lớn sẽ khiến người chơi bỏ cuộc; hãy thử nhiều giá trị.
  • Xoay nhẹ chú chim theo vận tốc dọc để tạo cảm giác bay.
  • Lưu điểm cao nhất bằng PlayerPrefs.
  • Thêm âm thanh khi vỗ cánh, ghi điểm và va chạm.
  • Tăng tốc ống dần theo điểm số để ván chơi có nhịp tăng tiến.

Kết luận

Bạn đã ghép bốn mảnh cơ bản: nhân vật vật lý, sinh và hủy đối tượng, trigger tính điểm và quản lý trạng thái ván chơi. Hầu hết game casual đều xây trên bộ khung tương tự.

Để học làm game trọn vẹn từ ý tưởng đến phát hành, hãy xem các khóa học Unity tại codebem.com.

author_photo
Nguyễn Đình Cường

0 comment