🐦 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ộngtcjsgame-perf.jsnế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
Componentvớiphysics = truevà 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
Componentdi 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
<!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 = truevàgravity/gravitySpeed. Khi người chơi đập cánh, chúng tôi thiết lậpbird.speedYthành một số âm để đẩy chim lên.hitBottom()vàbouncecó 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ậtComponentđược đặt bên ngoài cạnh phải và thiết lậpspeedXâm để chúng di chuyển sang trái. Chúng tôi giữ mảngpipesriê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
commcủa engine (hỗ trợremoveComponentFromComm()). - Ghi điểm: Chúng tôi đánh dấu cặp ống là
scoredmộ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.jsvà gọienableTCJSPerf(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ớidttrongmove(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_SPEEDvà/hoặc giảmPIPE_GAPkhi đ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ó trongcommvà đượ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.