Làm game rắn săn mồi bằng canvas JavaScript: vòng lặp game và va chạm

  • author-image

    Nguyễn Đình Cường

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

Game rắn săn mồi là bài tập tuyệt vời để hiểu cách một trò chơi hoạt động: vòng lặp game, trạng thái, nhập liệu và phát hiện va chạm. Bài này hướng dẫn bạn làm game hoàn chỉnh với thẻ canvas của HTML5, không cần thư viện nào.

Các khái niệm cần nắm

Mọi game đều xoay quanh một vòng lặp lặp đi lặp lại: cập nhật trạng thái (con rắn đi một bước, kiểm tra ăn mồi, kiểm tra thua) rồi vẽ lại màn hình. Với rắn săn mồi, trạng thái gồm: danh sách các ô thân rắn, hướng đi, vị trí con mồi và điểm số. Ta chia màn hình thành lưới ô vuông, nên mọi vị trí chỉ là cặp số nguyên (x, y).

Bước 1: HTML và CSS

<canvas id="game" width="400" height="400"></canvas>
<p>Điểm: <span id="score">0</span></p>
canvas { display: block; margin: 20px auto; background: #0f172a; border-radius: 8px; }
p { text-align: center; font-family: system-ui, sans-serif; }

Bước 2: Khởi tạo trạng thái

const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const scoreEl = document.getElementById('score');

const TILE = 20;                       // kích thước mỗi ô
const COLS = canvas.width / TILE;      // 20 cột
const ROWS = canvas.height / TILE;     // 20 hàng

let snake, direction, nextDirection, food, score, timer, running;

function reset() {
  snake = [{ x: 10, y: 10 }, { x: 9, y: 10 }, { x: 8, y: 10 }];
  direction = { x: 1, y: 0 };
  nextDirection = direction;
  score = 0;
  scoreEl.textContent = score;
  placeFood();
  running = true;
}

function placeFood() {
  while (true) {
    const pos = {
      x: Math.floor(Math.random() * COLS),
      y: Math.floor(Math.random() * ROWS),
    };
    // tránh đặt mồi lên thân rắn
    if (!snake.some((s) => s.x === pos.x && s.y === pos.y)) {
      food = pos;
      return;
    }
  }
}

Bước 3: Cập nhật mỗi bước

Mỗi nhịp, ta tính vị trí đầu mới bằng cách cộng hướng đi, thêm đầu mới vào đầu mảng. Nếu ăn mồi thì giữ nguyên đuôi (rắn dài ra), nếu không thì bỏ ô cuối (rắn dịch chuyển).

function update() {
  direction = nextDirection;
  const head = {
    x: snake[0].x + direction.x,
    y: snake[0].y + direction.y,
  };

  // Va chạm tường
  if (head.x < 0 || head.x >= COLS || head.y < 0 || head.y >= ROWS) {
    return gameOver();
  }
  // Va chạm thân
  if (snake.some((s) => s.x === head.x && s.y === head.y)) {
    return gameOver();
  }

  snake.unshift(head);

  if (head.x === food.x && head.y === food.y) {
    score++;
    scoreEl.textContent = score;
    placeFood();
  } else {
    snake.pop();
  }
}

function gameOver() {
  running = false;
  clearInterval(timer);
  ctx.fillStyle = 'rgba(0,0,0,0.6)';
  ctx.fillRect(0, 0, canvas.width, canvas.height);
  ctx.fillStyle = '#fff';
  ctx.font = '24px system-ui';
  ctx.textAlign = 'center';
  ctx.fillText('Thua rồi! Nhấn Space để chơi lại', canvas.width / 2, canvas.height / 2);
}

Bước 4: Vẽ

function draw() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  ctx.fillStyle = '#ef4444';
  ctx.fillRect(food.x * TILE, food.y * TILE, TILE - 2, TILE - 2);

  snake.forEach((part, i) => {
    ctx.fillStyle = i === 0 ? '#22c55e' : '#4ade80';
    ctx.fillRect(part.x * TILE, part.y * TILE, TILE - 2, TILE - 2);
  });
}

function tick() {
  update();
  if (running) draw();
}

Bước 5: Điều khiển và khởi động

Một lỗi kinh điển: nếu người chơi bấm hai phím rất nhanh trong cùng một nhịp (ví dụ phải rồi lên rồi trái), rắn có thể quay đầu 180 độ và tự cắn mình. Cách tránh là so sánh với hướng đã thực sự dùng ở nhịp trước (biến direction) chứ không phải hướng vừa bấm.

document.addEventListener('keydown', (e) => {
  const keys = {
    ArrowUp:    { x: 0, y: -1 },
    ArrowDown:  { x: 0, y: 1 },
    ArrowLeft:  { x: -1, y: 0 },
    ArrowRight: { x: 1, y: 0 },
  };

  if (keys[e.key]) {
    e.preventDefault();
    const d = keys[e.key];
    // không cho quay ngược lại
    if (d.x !== -direction.x || d.y !== -direction.y) {
      nextDirection = d;
    }
  } else if (e.code === 'Space' && !running) {
    start();
  }
});

function start() {
  reset();
  clearInterval(timer);
  timer = setInterval(tick, 120);
  draw();
}

start();

Hướng cải tiến

  • Tăng tốc độ dần khi điểm cao bằng cách tạo lại setInterval với chu kỳ ngắn hơn.
  • Cho rắn xuyên tường bằng phép chia lấy dư.
  • Lưu điểm cao nhất vào localStorage.
  • Thêm nút điều khiển cảm ứng để chơi trên điện thoại.
  • Chuyển sang requestAnimationFrame với bộ đếm thời gian để chuyển động mượt hơn.

Kết luận

Chưa đầy 100 dòng, bạn đã có một game đầy đủ vòng lặp, nhập liệu, va chạm và điểm số. Cấu trúc "trạng thái, cập nhật, vẽ" bạn vừa dùng là xương sống của game ở mọi nền tảng, từ canvas đến Unity.

Nếu muốn chuyển từ game web đơn giản sang làm game chuyên nghiệp hơn, hãy xem các khóa học tại codebem.com.

author_photo
Nguyễn Đình Cường

0 comment