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

Daniel X. moore
Daniel X. Moore

Giới thiệu

Bạn có muốn tạo trò chơi bằng Canvas và HTML5 không? Hãy làm theo hướng dẫn này và bạn sẽ nhanh chóng có thể bắt đầu hành trình.

Hướng dẫn giả định ít nhất một mức 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 rồi xem mã nguồn của trò chơi.

Tạo canvas

Để vẽ mọi thứ, chúng ta cần tạo một canvas. Vì đây là hướng dẫn Không cần nước mắt, 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 của lối chơi mượt mà và liên tục, chúng tôi muốn cập nhật trò chơi và vẽ lại màn hình nhanh hơn so với khả năng nhận biết của mắt và trí người.

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ẽ xử lý các lệnh gọi định kỳ.

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

Hello world

Giờ đây khi 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 này khá thú vị với văn bản tĩnh. Tuy nhiên, do đã thiết lập vòng lặp trò chơi, nên có thể làm cho văn bản di chuyển khá dễ dàng.

var textX = 50;
var textY = 50;

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

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

Giờ thì hãy vuốt nhẹ thôi. Nếu bạn đang theo dõi, nó sẽ di chuyển, nhưng cũng rời khỏ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. Điều này là do 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);
}

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

Tạo trình phát

Tạo một đối tượng để chứa dữ liệu của người chơi và chịu trách nhiệm cho các thao tác như vẽ. Ở đây, chúng ta tạo một đối tượng trình phát bằng cách sử dụng một đối tượng cố định đơn giản để lưu giữ mọi 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 tôi sử dụng một hình chữ nhật có màu đơn giản để biểu thị trình phát. Khi vẽ trò chơi, chúng ta sẽ xoá canvas và vẽ trình phát.

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

Điều khiển bằng bàn phím

Sử dụng phím nóng jQuery

Trình bổ trợ phím nóng jQuery giúp việc xử lý khoá trên các trình duyệt trở nên dễ dàng hơn nhiều. Thay vì khóc vì không hiểu rõ các vấn đề keyCodecharCode 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() { ... });

Không phải lo lắng về chi tiết về những khoá có mã nào đã giành chiến thắng lớn. Chúng tôi 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ì đó". Phím nóng jQuery cho phép điều đó tuyệt vời.

Chuyển động của người chơi

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

Có thể bạn đang hỏi "Tại sao không chỉ sử dụng cách xử lý khoá dựa trên sự kiện?" Đó là do tốc độ lặp lại bàn phím thay đổi giữa các hệ thống và không bị ràng buộc với thời gian của vòng lặp trò chơi, vì vậy lối chơi có thể khác nhau đáng kể giữa các hệ thống. Để tạo trải nghiệm nhất quán, bạn phải tích hợp chặt chẽ tính năng phát hiện sự kiện trên bàn phím với vòng lặp trò chơi.

Tin vui là tôi đã bao gồ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. Khoá này có tên là key_status.js và bạn có thể truy vấn trạng thái của 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ị lỗi 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 trình phát.

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

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

Hãy thử xem.

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

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 nhiều đầu vào cũng sẽ dễ dàng hơn, 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 đối tượng trò chơi khác

Đạn

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

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ể dấu đầu dòng và thêm thực thể đó vào tập hợp các viên đạ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 cập nhật dấu đầu dòng vào hàm bước cập nhật. Để ngăn dấu đầu dòng không bị lấp đầy vô thời hạn, chúng tôi 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. Việc này cũng cho phép chúng ta loại bỏ những 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ù

Giờ là lúc thêm kẻ thù theo cách tương tự như cách chúng ta thêm đường đạ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 tuyệt khi xem tất cả những chiếc hộp đó bay quanh, nhưng ảnh cho chúng sẽ càng tuyệt vời hơn. Việc tải và vẽ hình ảnh trên canvas thường là một trải nghiệm nước mắt. Để ngăn chặn sự đau khổ và sự đ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 thấy tất cả các giao dịch này di chuyển xung quanh trên màn hình, nhưng chúng không tương tác với nhau. Để thông báo cho mọi thứ biết thời điểm nổ, chúng ta cần thêm một hình thức phát hiện va chạm.

Hãy sử dụng một 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 số xung đột mà chúng ta muốn kiểm tra:

  1. Đạn đạn của người chơi => Phi thuyền của kẻ thù
  2. Người chơi => Phi thuyền của kẻ thù

Hãy tạo một phương thức để xử lý các xung đột 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 pháp phát nổ cho người chơi và kẻ thù. Thao tác này sẽ gắn cờ để loại bỏ và thêm một vụ 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 hấp dẫn. Â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 audio.js kỳ diệu của chúng tôi, âm thanh có thể được tạo trở nên 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ị xé hình, nhưng việc thêm âm thanh hiện là cách nhanh nhất để gây ra sự cố cho ứng dụng của bạn. Âm thanh bị cắt bớt hoặc gỡ bỏ toàn bộ thẻ trình duyệt là điều bình thường. Vì vậy, hãy chuẩn bị sẵn mô hình của bạn.

Tạm biệt

Xin nhắc lại, đây là bản minh hoạ trò chơi làm việc đầ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 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 tôi có thể tự cách ly bản thân khỏi những phần khó khăn hơn của API, cũng như kiên cường khi đối mặt với những thay đổi trong tương lai.

Tài liệu tham khảo