0
0
Lập trình
Admin Team
Admin Teamtechmely

🐦 Xây Dựng Flappy Bird với TCJSgame v3 — Hướng Dẫn Chi Tiết

Đăng vào 7 tháng trước

• 13 phút đọc

🐦 Xây Dựng Flappy Bird với TCJSgame v3 — Hướng Dẫn Chi Tiết

Hướng dẫn này sẽ chỉ bạn cách tạo một phiên bản đơn giản của trò chơi Flappy Bird bằng TCJSgame v3. Trò chơi sử dụng một "con chim" hình chữ nhật, các ống được sinh ra ngẫu nhiên, phát hiện va chạm với crashWith(), cơ chế trọng lực và đập cánh đơn giản, điểm số và logic khởi động lại.

Giả sử rằng tcjsgame-v3.js đã được tải lên cục bộ hoặc từ trang web của bạn (có thể thêm phần mở rộng tcjsgame-perf.js nếu bạn muốn sử dụng requestAnimationFrame và thời gian delta).


Nội dung bạn sẽ học

  • Thiết lập hiển thị và thành phần TCJSgame
  • Triển khai trọng lực và điều khiển đập cánh
  • Sinh ra các ống di chuyển với khoảng trống ngẫu nhiên
  • Phát hiện va chạm và khởi động lại trò chơi
  • Theo dõi và hiển thị điểm số

1. Cấu trúc dự án (tập tin đơn)

Tất cả mã dưới đây được chứa trong một tập tin HTML để dễ dàng kiểm tra. Lưu với tên flappy.html và mở trong trình duyệt.


2. Ý tưởng (nhanh chóng)

  • Con chim là một Component với physics = true và thiết lập trọng lực.
  • Nhấp chuột hoặc nhấn phím Space sẽ làm con chim "đập cánh" (tăng lực hướng lên).
  • Các ống là các hình chữ nhật Component di chuyển sang trái và được sinh ra định kỳ; mỗi cặp có một khoảng trống.
  • Nếu con chim crashWith() bất kỳ ống nào hoặc va chạm với trên/dưới, trò chơi sẽ kết thúc.
  • Điểm số tăng lên khi con chim vượt qua một ống.

3. Mã hoàn chỉnh (sao chép & dán)

html Copy
<!doctype html>
<html>
<head>
  <meta charset="utf-8" />
  <title>Flappy Bird — TCJSgame v3</title>
  <style>
    /* kiểu dáng trang tối thiểu */
    body { margin:0; font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial; background:#111; color:#eee; display:flex; align-items:center; justify-content:center; height:100vh; }
    .wrap { width: 900px; max-width: 100%; }
    #hud { display:flex; justify-content:space-between; margin-bottom:8px; }
    #score { font-weight:700; font-size:18px; }
    #message { font-size:14px; opacity:0.9; }
    canvas { display:block; border-radius:8px; box-shadow:0 8px 30px rgba(0,0,0,0.6); border:6px solid rgba(255,255,255,0.02); background: linear-gradient(#87CEEB,#cfeefe); }
    .btn { background:#1e88e5; color:white; padding:8px 12px; border-radius:6px; cursor:pointer; border:0; }
  </style>
</head>
<body>
  <div class="wrap">
    <div id="hud">
      <div id="score">Điểm: 0</div>
      <div id="message">Nhấn Space hoặc nhấp/touch để đập cánh — tránh các ống</div>
    </div>

    <!-- bao gồm engine -->
    <script src="tcjsgame-v3.js"></script>
    <!-- Tùy chọn: bao gồm phần mở rộng hiệu suất để sử dụng requestAnimationFrame + thời gian delta -->
    <!-- <script src="https://tcjsgame.vercel.app/mat/tcjsgame-perf.js"></script> -->

    <script>
    // ---------- Hằng số trò chơi ----------
    const CANVAS_W = 900;
    const CANVAS_H = 600;
    const PIPE_WIDTH = 80;
    const PIPE_GAP = 180;         // khoảng cách dọc giữa ống trên và ống dưới
    const PIPE_SPACING = 1500;    // ms giữa các lần sinh
    const PIPE_SPEED = 3;         // pixel trên mỗi khung (tăng với độ khó)
    const GRAVITY = 0.35;
    const FLAP_STRENGTH = -6.5;

    // ---------- Biến toàn cục ----------
    const display = new Display();
    display.start(CANVAS_W, CANVAS_H);
    // Nếu bạn đã bao gồm phần mở rộng perf, hãy kích hoạt thời gian delta:
    // enableTCJSPerf(display, { useDelta:false, cacheTiles:false, cullMargin: 32 });

    let scoreEl = document.getElementById('score');
    let msgEl = document.getElementById('message');

    // Thành phần chim
    let bird = new Component(36, 26, "orange", 140, 200, "rect");
    bird.physics = true;
    bird.gravity = GRAVITY;
    bird.speedX = 0;
    bird.speedY = 0;
    bird.bounce = 0.2;
    display.add(bird);

    // Container ống (chúng ta sẽ theo dõi chính mình để có thể xóa)
    let pipes = []; // mỗi ống là một đối tượng { top:Component, bottom:Component, scored:false }

    // Tiện ích: xóa một thành phần khỏi comm toàn cục để nó không còn được vẽ/cập nhật
    function removeComponentFromComm(comp) {
      for (let i = comm.length - 1; i >= 0; i--) {
        if (comm[i].x === comp) {
          comm.splice(i, 1);
          break;
        }
      }
    }

    // Sinh ra một cặp ống (trên và dưới) với vị trí khoảng trống ngẫu nhiên
    function spawnPipes() {
      const gapTop = 80 + Math.random() * (CANVAS_H - 240 - PIPE_GAP); // vị trí trên của khoảng trống
      const xStart = display.canvas.width + 40;

      // ống trên (chiều cao = gapTop)
      let topPipe = new Component(PIPE_WIDTH, gapTop, "green", xStart, 0, "rect");
      topPipe.speedX = -PIPE_SPEED;
      topPipe.physics = false;
      display.add(topPipe);

      // ống dưới (y = gapTop + PIPE_GAP)
      let bottomPipeY = gapTop + PIPE_GAP;
      let bottomH = CANVAS_H - bottomPipeY;
      let bottomPipe = new Component(PIPE_WIDTH, bottomH, "green", xStart, bottomPipeY, "rect");
      bottomPipe.speedX = -PIPE_SPEED;
      bottomPipe.physics = false;
      display.add(bottomPipe);

      pipes.push({ top: topPipe, bottom: bottomPipe, scored: false });
    }

    // Xóa tất cả các ống
    function clearPipes() {
      pipes.forEach(p => {
        removeComponentFromComm(p.top);
        removeComponentFromComm(p.bottom);
      });
      pipes = [];
    }

    // Đặt lại trạng thái trò chơi
    let score = 0;
    let lastSpawn = performance.now();
    let running = true;
    function resetGame() {
      // đặt lại chim
      bird.x = 140;
      bird.y = 200;
      bird.speedX = 0;
      bird.speedY = 0;
      bird.gravitySpeed = 0;
      bird.physics = true;
      // xóa ống và đặt lại điểm số
      clearPipes();
      score = 0;
      updateScore();
      lastSpawn = performance.now() + 500;
      running = true;
      msgEl.textContent = "Nhấn Space hoặc nhấp/touch để đập cánh — tránh các ống";
    }

    function updateScore() {
      scoreEl.textContent = "Điểm: " + score;
    }

    // Hành động đập cánh
    function flap() {
      bird.speedY = FLAP_STRENGTH;
    }

    // Bộ xử lý nhập liệu
    window.addEventListener("keydown", (e) => {
      if (e.code === "Space") {
        if (!running) { resetGame(); return; }
        flap();
      }
      // tùy chọn phím mũi tên lên
      if (e.keyCode === 38) flap();
    });

    // Nhấp / chạm để đập cánh hoặc khởi động lại
    display.canvas.addEventListener("mousedown", (e) => {
      if (!running) { resetGame(); return; }
      flap();
    });
    display.canvas.addEventListener("touchstart", (e) => {
      e.preventDefault();
      if (!running) { resetGame(); return; }
      flap();
    }, { passive:false });

    // Hàm cập nhật toàn cục được gọi bởi TCJSgame (v3)
    function update(dt) {
      // dt có thể được truyền bởi perf-extension (trong giây). Nếu không có, hãy bỏ qua dt và sử dụng tốc độ dựa trên khung.
      const useDt = typeof dt === "number";

      // Áp dụng trọng lực (nếu thành phần vật lý sử dụng gravitySpeed khi di chuyển)
      if (useDt) {
        // Nếu phần mở rộng perf cung cấp dt, hãy chuyển đổi tốc độ thành px/sec (chúng tôi hiểu tốc độ của mình là px trên mỗi khung trong phiên bản đơn giản này,
        // do đó chúng tôi giữ tỷ lệ nhất quán bằng cách nhân với 60 cho các giá trị cũ — để đơn giản, chúng tôi chỉ cập nhật vị trí trực tiếp với vật lý.)
        bird.gravitySpeed += bird.gravity * dt * 60;
        bird.y += bird.speedY * dt * 60 + bird.gravitySpeed;
      } else {
        // hành vi theo khung gốc
        bird.gravitySpeed += bird.physics ? bird.gravity : 0;
        bird.y += bird.speedY + bird.gravitySpeed;
      }

      // đảm bảo chim không quay hoặc di chuyển theo chiều ngang
      // không sử dụng chuyển động ngang trong flappy

      // sinh ống định kỳ
      const now = performance.now();
      if (now - lastSpawn > PIPE_SPACING) {
        spawnPipes();
        lastSpawn = now;
      }

      // di chuyển ống & kiểm tra xem có nằm ngoài màn hình và ghi điểm không
      for (let i = pipes.length - 1; i >= 0; i--) {
        const pair = pipes[i];
        // di chuyển mỗi ống (chúng đã có speedX được thiết lập và sẽ được di chuyển bởi component.move() trong vòng lặp engine)
        // Nhưng chúng tôi cần tăng x của chúng một cách thủ công nếu sử dụng chế độ dt (nếu component.move mong đợi không dt)
        // Thay vào đó dựa vào speedX của chúng và move() của engine sẽ tăng theo mỗi khung.
        // Xóa các ống đã hoàn toàn ra ngoài màn hình
        if (pair.top.x + pair.top.width < -50) {
          // xóa cả hai
          removeComponentFromComm(pair.top);
          removeComponentFromComm(pair.bottom);
          pipes.splice(i, 1);
          continue;
        }

        // ghi điểm: khi cạnh bên phải của ống đi qua chim và chưa ghi điểm
        if (!pair.scored && pair.top.x + pair.top.width < bird.x) {
          pair.scored = true;
          score += 1;
          updateScore();
        }

        // va chạm: nếu chim va chạm với bất kỳ ống nào -> trò chơi kết thúc
        if (pair.top.crashWith(bird) || pair.bottom.crashWith(bird)) {
          gameOver();
        }
      }

      // kiểm tra giới hạn trên/dưới
      if (bird.y < -20 || bird.y + bird.height > display.canvas.height + 20) {
        // chim ra ngoài giới hạn
        gameOver();
      }
    }

    function gameOver() {
      if (!running) return;
      running = false;
      msgEl.textContent = "Game Over — Nhấp hoặc nhấn Space để khởi động lại";
      // dừng tất cả chuyển động của ống
      pipes.forEach(p => {
        p.top.speedX = 0;
        p.bottom.speedX = 0;
      });
      // tùy chọn, hiển thị một cú nhảy nhỏ
      bird.speedY = 0;
      bird.gravitySpeed = 0;
    }

    // khởi tạo trò chơi
    resetGame();

    // Lưu ý: display.update() đã được lập lịch bởi lệnh start() của engine.
    // Hàm cập nhật(dt) toàn cục ở trên sẽ được gọi mỗi lần tick.
    </script>
  </div>
</body>
</html>

4. Giải thích & mẹo

  • Trọng lực & đập cánh: Con chim sử dụng physics = truegravity/gravitySpeed. Khi người chơi đập cánh, chúng tôi thiết lập bird.speedY thành một số âm để đẩy chim lên. hitBottom()bounce có thể được sử dụng nếu bạn muốn có vật lý bật lại; ở đây chúng tôi coi việc va chạm với mặt đất là trò chơi kết thúc.
  • Sinh ống: spawnPipes() tạo ra hai hình chữ nhật Component được đặt bên ngoài cạnh phải và thiết lập speedX âm để chúng di chuyển sang trái. Chúng tôi giữ mảng pipes riêng để dễ dàng quản lý điểm số và xóa.
  • Va chạm: crashWith() hoạt động cho các hình chữ nhật đã xoay hoặc dịch chuyển — chúng tôi sử dụng điều đó để phát hiện va chạm giữa chim và ống.
  • Khởi động lại & dọn dẹp: Để xóa một thành phần khỏi việc vẽ/cập nhật, chúng tôi cắt nó từ mảng comm của engine (hỗ trợ removeComponentFromComm()).
  • Ghi điểm: Chúng tôi đánh dấu cặp ống là scored một khi cạnh bên phải của ống đi qua chim để tránh tính điểm gấp đôi.

5. Cải tiến tùy chọn

  • Sử dụng phần mở rộng perf + dt: bao gồm tcjsgame-perf.js và gọi enableTCJSPerf(display, { useDelta:true }). Sau đó chuyển đổi toán học di chuyển thành px/sec thực bằng cách điều chỉnh tốc độ và nhân với dt trong move(dt).
  • Tinh chỉnh hình ảnh: thay thế ống và chim hình chữ nhật bằng sprite hoặc hình ảnh.
  • Thêm âm thanh: let s = new Sound("flap.wav"); s.play() khi đập cánh và phát một âm thanh va chạm khi trò chơi kết thúc.
  • Đường cong độ khó: tăng PIPE_SPEED và/hoặc giảm PIPE_GAP khi điểm số tăng lên.
  • Thân thiện với di động: thêm nút chạm trên màn hình, điều chỉnh trọng lực và sức mạnh đập cánh.

6. Khắc phục sự cố

  • Nếu ống không di chuyển, hãy đảm bảo bạn đã thêm chúng với display.add(pipe) để chúng có trong comm và được cập nhật mỗi khung.
  • Nếu va chạm cảm thấy không chính xác, hãy kiểm tra kích thước & vị trí của các thành phần và xem xét sử dụng các hộp va chạm nhỏ hơn một chút để đảm bảo trò chơi công bằng.
  • Nếu engine chạy quá nhanh trên các màn hình có tần số làm mới cao, hãy bao gồm phần mở rộng perf và kích hoạt useDelta.

7. Ghi chú cuối

Phiên bản này sử dụng chuyển động theo khung đơn giản nên nó chạy với engine v3 gốc (cái mà sử dụng setInterval theo mặc định). Để có chất lượng sản xuất, hãy tích hợp phần mở rộng tcjsgame-perf.js và di chuyển chuyển động sang thời gian delta để tốc độ trò chơi nhất quán trên các thiết bị khác nhau.

Gợi ý câu hỏi phỏng vấn
Không có dữ liệu

Không có dữ liệu

Bài viết được đề xuất
Bài viết cùng tác giả

Bình luận

Chưa có bình luận nào

Chưa có bình luận nào