Hướng dẫn đơn giản về trò chơi HTML5

Daniel X. Moore
Daniel X. Moore

Giới thiệu

Vậy bạn muốn tạo một trò chơi bằng Canvas và HTML5? Hãy làm theo hướng dẫn này và bạn sẽ sớm bắt đầu sử dụng được.

Hướng dẫn này giả định bạn có ít nhất kiến thức trung cấp về JavaScript.

Trước tiên, bạn có thể chơi trò chơi hoặc chuyển thẳng đến bài viết và xem mã nguồn của trò chơi.

Tạo canvas

Để vẽ các đối tượng, chúng ta cần tạo một canvas. Vì đây là hướng dẫn Không gây khó khăn, nên chúng ta sẽ sử dụng jQuery.

var CANVAS_WIDTH = 480;
var CANVAS_HEIGHT = 320;

var canvasElement = $("<canvas width='" + CANVAS_WIDTH + 
                      "' height='" + CANVAS_HEIGHT + "'></canvas>");
var canvas = canvasElement.get(0).getContext("2d");
canvasElement.appendTo('body');

Vòng lặp trò chơi

Để mô phỏng giao diện trò chơi mượt mà và liên tục, chúng ta muốn cập nhật trò chơi và vẽ lại màn hình nhanh hơn so với tốc độ mà tâm trí và mắt người có thể nhận biết.

var FPS = 30;
setInterval(function() {
  update();
  draw();
}, 1000/FPS);

Hiện tại, chúng ta có thể để trống phương thức cập nhật và vẽ. Điều quan trọng cần biết là setInterval() sẽ gọi các phương thức này theo định kỳ.

function update() { ... }
function draw() { ... }

Hello world

Bây giờ, chúng ta đã có một vòng lặp trò chơi, hãy cập nhật phương thức vẽ để thực sự vẽ một số văn bản trên màn hình.

function draw() {
  canvas.fillStyle = "#000"; // Set color to black
  canvas.fillText("Sup Bro!", 50, 50);
}

Điều đó khá thú vị đối với văn bản tĩnh, nhưng vì chúng ta đã thiết lập một vòng lặp trò chơi, nên chúng ta có thể dễ dàng di chuyển văn bản đó.

var textX = 50;
var textY = 50;

function update() {
  textX += 1;
  textY += 1;
}

function draw() {
  canvas.fillStyle = "#000";
  canvas.fillText("Sup Bro!", textX, textY);
}

Bây giờ, hãy thử nghiệm. Nếu bạn đang làm theo, hình ảnh sẽ di chuyển, nhưng cũng để lại các lần vẽ trước đó trên màn hình. Hãy dành chút thời gian để đoán lý do có thể xảy ra trường hợp đó. Lý do là chúng ta không xoá màn hình. Vì vậy, hãy thêm một số mã xoá màn hình vào phương thức vẽ.

function draw() {
  canvas.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
  canvas.fillStyle = "#000";
  canvas.fillText("Sup Bro!", textX, textY);
}

Bây giờ, bạn đã có một số văn bản di chuyển trên màn hình, tức là bạn đã đi được một nửa chặng đường để có một trò chơi thực sự. Chỉ cần thắt chặt các nút điều khiển, cải thiện lối chơi, làm mới đồ hoạ. Ok, có thể bạn đã đi được 1/7 chặng đường để có một trò chơi thực sự, nhưng tin vui là hướng dẫn này còn nhiều điều khác nữa.

Tạo trình phát

Tạo một đối tượng để lưu trữ dữ liệu người chơi và chịu trách nhiệm về các hoạt động như vẽ. Ở đây, chúng ta tạo một đối tượng người chơi bằng cách sử dụng một giá trị cố định đối tượng đơn giản để lưu trữ tất cả thông tin.

var player = {
  color: "#00A",
  x: 220,
  y: 270,
  width: 32,
  height: 32,
  draw: function() {
    canvas.fillStyle = this.color;
    canvas.fillRect(this.x, this.y, this.width, this.height);
  }
};

Hiện tại, chúng ta đang sử dụng một hình chữ nhật có màu đơn giản để biểu thị người chơi. Khi vẽ trò chơi, chúng ta sẽ xoá canvas và vẽ người chơi.

function draw() {
  canvas.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
  player.draw();
}

Các nút điều khiển trên bàn phím

Sử dụng phím tắt jQuery

Trình bổ trợ phím nóng jQuery giúp việc xử lý phím trên các trình duyệt trở nên dễ dàng hơn nhiều. Thay vì khóc lóc vì các vấn đề keyCodecharCode không thể giải mã trên nhiều trình duyệt, chúng ta có thể liên kết các sự kiện như sau:

$(document).bind("keydown", "left", function() { ... });

Việc không phải lo lắng về chi tiết về khoá nào có mã nào là một thành công lớn. Chúng ta chỉ muốn có thể nói những câu như "khi người chơi nhấn nút lên, hãy làm gì đó". jQuery Hotkeys cho phép điều đó một cách dễ dàng.

Di chuyển người chơi

Cách JavaScript xử lý các sự kiện bàn phím hoàn toàn là do sự kiện điều khiển. Điều đó có nghĩa là không có truy vấn tích hợp sẵn để kiểm tra xem một khoá có bị lỗi hay không, vì vậy chúng ta sẽ phải sử dụng truy vấn của riêng mình.

Bạn có thể thắc mắc: "Tại sao không chỉ sử dụng cách xử lý phím do sự kiện điều khiển?" Lý do là tốc độ lặp lại của bàn phím thay đổi tuỳ theo hệ thống và không liên quan đến thời gian của vòng lặp trò chơi, vì vậy, cách chơi có thể khác nhau rất nhiều giữa các hệ thống. Để tạo ra trải nghiệm nhất quán, điều quan trọng là bạn phải tích hợp chặt chẽ tính năng phát hiện sự kiện bàn phím với vòng lặp trò chơi.

Tin vui là tôi đã thêm một trình bao bọc JS gồm 16 dòng để cung cấp tính năng truy vấn sự kiện. Tệp này có tên là key_status.js và bạn có thể truy vấn trạng thái của một khoá bất cứ lúc nào bằng cách kiểm tra keydown.left, v.v.

Giờ đây, chúng ta có thể truy vấn xem các phím có bị nhấn hay không, chúng ta có thể sử dụng phương thức cập nhật đơn giản này để di chuyển người chơi.

function update() {
  if (keydown.left) {
    player.x -= 2;
  }

  if (keydown.right) {
    player.x += 2;
  }
}

Hãy tiếp tục và thử.

Bạn có thể nhận thấy rằng trình phát có thể được di chuyển ra khỏi màn hình. Hãy cố định vị trí của người chơi để giữ họ trong giới hạn. Ngoài ra, người chơi có vẻ hơi chậm, vì vậy, hãy tăng tốc độ lên.

function update() {
  if (keydown.left) {
    player.x -= 5;
  }

  if (keydown.right) {
    player.x += 5;
  }

  player.x = player.x.clamp(0, CANVAS_WIDTH - player.width);
}

Việc thêm các phương thức nhập khác cũng sẽ dễ dàng như vậy, vì vậy, hãy thêm một số loại đạn.

function update() {
  if (keydown.space) {
    player.shoot();
  }

  if (keydown.left) {
    player.x -= 5;
  }

  if (keydown.right) {
    player.x += 5;
  }

  player.x = player.x.clamp(0, CANVAS_WIDTH - player.width);
}

player.shoot = function() {
  console.log("Pew pew");
  // :) Well at least adding the key binding was easy...
};

Thêm các đối tượng trò chơi khác

Vũ khí ném

Bây giờ, hãy thêm các vật bắn thực sự. Trước tiên, chúng ta cần một bộ sưu tập để lưu trữ tất cả các giá trị này:

var playerBullets = [];

Tiếp theo, chúng ta cần một hàm khởi tạo để tạo các thực thể dấu đầu dòng.

function Bullet(I) {
  I.active = true;

  I.xVelocity = 0;
  I.yVelocity = -I.speed;
  I.width = 3;
  I.height = 3;
  I.color = "#000";

  I.inBounds = function() {
    return I.x >= 0 && I.x <= CANVAS_WIDTH &&
      I.y >= 0 && I.y <= CANVAS_HEIGHT;
  };

  I.draw = function() {
    canvas.fillStyle = this.color;
    canvas.fillRect(this.x, this.y, this.width, this.height);
  };

  I.update = function() {
    I.x += I.xVelocity;
    I.y += I.yVelocity;

    I.active = I.active && I.inBounds();
  };

  return I;
}

Khi người chơi bắn, chúng ta nên tạo một thực thể đạn và thêm thực thể đó vào tập hợp đạn.

player.shoot = function() {
  var bulletPosition = this.midpoint();

  playerBullets.push(Bullet({
    speed: 5,
    x: bulletPosition.x,
    y: bulletPosition.y
  }));
};

player.midpoint = function() {
  return {
    x: this.x + this.width/2,
    y: this.y + this.height/2
  };
};

Bây giờ, chúng ta cần thêm nội dung cập nhật các dấu đầu dòng vào hàm cập nhật bước. Để ngăn chặn việc bộ sưu tập dấu đầu dòng bị đầy vô thời hạn, chúng ta lọc danh sách dấu đầu dòng để chỉ bao gồm các dấu đầu dòng đang hoạt động. Điều này cũng cho phép chúng ta loại bỏ các viên đạn đã va chạm với kẻ thù.

function update() {
  ...
  playerBullets.forEach(function(bullet) {
    bullet.update();
  });

  playerBullets = playerBullets.filter(function(bullet) {
    return bullet.active;
  });
}

Bước cuối cùng là vẽ các dấu đầu dòng:

function draw() {
  ...
  playerBullets.forEach(function(bullet) {
    bullet.draw();
  });
}

Kẻ thù

Bây giờ, chúng ta sẽ thêm kẻ thù theo cách tương tự như cách thêm đạn.

  enemies = [];

function Enemy(I) {
  I = I || {};

  I.active = true;
  I.age = Math.floor(Math.random() * 128);

  I.color = "#A2B";

  I.x = CANVAS_WIDTH / 4 + Math.random() * CANVAS_WIDTH / 2;
  I.y = 0;
  I.xVelocity = 0
  I.yVelocity = 2;

  I.width = 32;
  I.height = 32;

  I.inBounds = function() {
    return I.x >= 0 && I.x <= CANVAS_WIDTH &&
      I.y >= 0 && I.y <= CANVAS_HEIGHT;
  };

  I.draw = function() {
    canvas.fillStyle = this.color;
    canvas.fillRect(this.x, this.y, this.width, this.height);
  };

  I.update = function() {
    I.x += I.xVelocity;
    I.y += I.yVelocity;

    I.xVelocity = 3 * Math.sin(I.age * Math.PI / 64);

    I.age++;

    I.active = I.active && I.inBounds();
  };

  return I;
};

function update() {
  ...

  enemies.forEach(function(enemy) {
    enemy.update();
  });

  enemies = enemies.filter(function(enemy) {
    return enemy.active;
  });

  if(Math.random() < 0.1) {
    enemies.push(Enemy());
  }
};

function draw() {
  ...

  enemies.forEach(function(enemy) {
    enemy.draw();
  });
}

Tải và vẽ hình ảnh

Thật thú vị khi xem tất cả các hộp đó bay xung quanh, nhưng sẽ còn thú vị hơn khi có hình ảnh cho các hộp đó. Việc tải và vẽ hình ảnh trên canvas thường là một trải nghiệm đầy nước mắt. Để tránh sự phiền toái và đau khổ đó, chúng ta có thể sử dụng một lớp tiện ích đơn giản.

player.sprite = Sprite("player");

player.draw = function() {
  this.sprite.draw(canvas, this.x, this.y);
};

function Enemy(I) {
  ...

  I.sprite = Sprite("enemy");

  I.draw = function() {
    this.sprite.draw(canvas, this.x, this.y);
  };

  ...
}

Phát hiện va chạm

Chúng ta có tất cả các dealies này bay xung quanh trên màn hình, nhưng chúng không tương tác với nhau. Để cho mọi thứ biết thời điểm nổ, chúng ta cần thêm một số loại tính năng phát hiện va chạm.

Hãy sử dụng thuật toán phát hiện va chạm hình chữ nhật đơn giản:

function collides(a, b) {
  return a.x < b.x + b.width &&
         a.x + a.width > b.x &&
         a.y < b.y + b.height &&
         a.y + a.height > b.y;
}

Có một vài xung đột mà chúng ta muốn kiểm tra:

  1. Đạn của người chơi => Tàu của kẻ thù
  2. Người chơi => Tàu địch

Hãy tạo một phương thức để xử lý các va chạm mà chúng ta có thể gọi từ phương thức cập nhật.

function handleCollisions() {
  playerBullets.forEach(function(bullet) {
    enemies.forEach(function(enemy) {
      if (collides(bullet, enemy)) {
        enemy.explode();
        bullet.active = false;
      }
    });
  });

  enemies.forEach(function(enemy) {
    if (collides(enemy, player)) {
      enemy.explode();
      player.explode();
    }
  });
}

function update() {
  ...
  handleCollisions();
}

Bây giờ, chúng ta cần thêm các phương thức nổ vào người chơi và kẻ thù. Thao tác này sẽ gắn cờ để xoá các thành phần đó và thêm một hiệu ứng nổ.

function Enemy(I) {
  ...

  I.explode = function() {
    this.active = false;
    // Extra Credit: Add an explosion graphic
  };

  return I;
};

player.explode = function() {
  this.active = false;
  // Extra Credit: Add an explosion graphic and then end the game
};

Âm thanh

Để hoàn thiện trải nghiệm, chúng ta sẽ thêm một số hiệu ứng âm thanh thú vị. Âm thanh, giống như hình ảnh, có thể hơi khó sử dụng trong HTML5, nhưng nhờ công thức sound.js kỳ diệu không gây khó chịu, bạn có thể tạo âm thanh một cách cực kỳ đơn giản.

player.shoot = function() {
  Sound.play("shoot");
  ...
}

function Enemy(I) {
  ...

  I.explode = function() {
    Sound.play("explode");
    ...
  }
}

Mặc dù API hiện không bị giật, nhưng việc thêm âm thanh hiện là cách nhanh nhất để làm ứng dụng của bạn gặp sự cố. Không có gì lạ khi âm thanh bị cắt hoặc toàn bộ thẻ trình duyệt bị gỡ bỏ, vì vậy, hãy chuẩn bị sẵn khăn giấy.

Farewell

Xin nhắc lại, đây là bản minh hoạ trò chơi hoạt động đầy đủ. Bạn cũng có thể tải mã nguồn xuống dưới dạng tệp zip.

Tôi hy vọng bạn đã thích thú khi tìm hiểu kiến thức cơ bản về cách tạo một trò chơi đơn giản bằng JavaScript và HTML5. Bằng cách lập trình ở cấp độ trừu tượng phù hợp, chúng ta có thể tách biệt với các phần khó hơn của API, cũng như có khả năng thích ứng khi đối mặt với các thay đổi trong tương lai.

Tài liệu tham khảo