คู่มือแนะนำเกม HTML5 แบบไม่มีสะดุด

แดเนียล เอ็กซ์ มัวร์
แดเนียล เอ็กซ์. มัวร์

เกริ่นนำ

ถ้าคุณอยากสร้างเกมโดยใช้ Canvas และ HTML5 ทำตามบทแนะนำนี้ไปเรื่อยๆ แล้วคุณจะกลับมาใหม่ทันที

บทแนะนำนี้ใช้ความรู้เบื้องต้นเกี่ยวกับ JavaScript ในระดับกลาง

ก่อนอื่น คุณสามารถเล่นเกมหรือข้ามไปยังบทความและดูซอร์สโค้ดของเกมได้โดยตรง

กำลังสร้างแคนวาส

เราจะต้องสร้างแคนวาสเพื่อวาดภาพ เพราะนี่เป็นคำแนะนำ ไม่มีน้ำตา เราจึงจะใช้ 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');

Game Loop

เพื่อจำลองลักษณะเกมเพลย์ที่ราบรื่นและต่อเนื่อง เราต้องการอัปเดตเกมและวาดหน้าจอใหม่เร็วกว่าที่มนุษย์และดวงตาจะสามารถรับรู้ได้

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

สำหรับตอนนี้เราสามารถเว้นว่างวิธีการอัปเดตและวาดได้ สิ่งสำคัญที่ควรทราบคือ setInterval() จะคอยโทรหาบุคคลเหล่านั้นเป็นระยะๆ

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

สวัสดีทุกคน

ตอนนี้เราเริ่ม Game Loop แล้ว มาอัปเดตวิธีการวาดเพื่อร่างข้อความบนหน้าจอกัน

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

ซึ่งถือว่าดีทีเดียวสำหรับโฆษณาแบบอยู่กับที่ แต่เนื่องจากเราได้ติดตั้ง Game Loop ไว้แล้ว เราจึงน่าจะทำให้การเคลื่อนที่เป็นไปได้ค่อนข้างง่าย

var textX = 50;
var textY = 50;

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

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

ทีนี้ลองหมุนค้างดู หากคุณติดตามอยู่ ภาพก็ควรจะเคลื่อนไหว แต่ยังคงอยู่เมื่อครั้งที่แล้วถูกวาดขึ้นบนหน้าจอ ลองใช้เวลาสักครู่เพื่อ เดาดูว่าเหตุใดจึงเป็นเช่นนั้น เนื่องจากเราไม่ได้ล้างหน้าจอ เรามาเพิ่มโค้ดการล้างหน้าจอลงในวิธีการวาดกันดีกว่า

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

ตอนนี้เมื่อได้เห็นข้อความที่เคลื่อนที่ไปมาบนหน้าจอ ก็มาถึงครึ่งทางไปสู่การเริ่มเล่นเกมจริงๆ แค่ปรับการควบคุมให้กระชับ ปรับปรุงเกมเพลย์ ตกแต่งกราฟิก โอเค อาจเป็น 1/7 ของวิธีสร้างเกมจริง แต่ ข่าวดีก็คือบทแนะนำยังมีอะไรอีกมากมาย

การสร้างโปรแกรมเล่น

สร้างวัตถุเพื่อเก็บข้อมูลผู้เล่นและรับผิดชอบต่อสิ่งต่างๆ เช่น การวาด ตรงนี้เราสร้างออบเจ็กต์โปรแกรมเล่นโดยใช้ Object Lite แบบง่ายๆ เพื่อเก็บข้อมูลทั้งหมด

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);
  }
};

ตอนนี้เราใช้สี่เหลี่ยมผืนผ้าสีง่ายๆ เพื่อแสดงแทนผู้เล่น เมื่อเราวาดรูปเกม เราจะล้างผืนผ้าใบและวาดผู้เล่น

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

การควบคุมด้วยแป้นพิมพ์

การใช้ Hotkey ของ jQuery

ปลั๊กอิน jQuery Hotkeys ทำให้การจัดการคีย์ในเบราว์เซอร์ต่างๆ ง่ายขึ้นมาก แทนที่จะร้องไห้กับปัญหา keyCode และ charCode แบบหลายเบราว์เซอร์ที่ถอดรหัสไม่ได้ เราสามารถเชื่อมโยงเหตุการณ์ได้ดังนี้

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

โดยไม่ต้องกังวลเกี่ยวกับรายละเอียดว่าคีย์ใดมีรหัสใดจะได้ผลดีที่สุด เราอยากจะพูดประโยคอย่างเช่น "เมื่อผู้เล่นกดปุ่มขึ้น ทำอะไรบางอย่าง" jQuery Hotkey ก็ทำได้

การเคลื่อนที่ของผู้เล่น

วิธีที่ JavaScript จัดการเหตุการณ์บนแป้นพิมพ์จะขับเคลื่อนด้วยเหตุการณ์โดยสมบูรณ์ ซึ่งหมายความว่าไม่มีการค้นหาในตัวสำหรับการตรวจสอบว่าคีย์หยุดทำงานหรือไม่ เราจึงต้องใช้คีย์ของเราเอง

คุณอาจสงสัยว่า "ทำไมไม่ใช้วิธีจัดการคีย์ที่ขับเคลื่อนด้วยเหตุการณ์" นั่นเป็นเพราะอัตราการเล่นของแป้นพิมพ์ซ้ำแตกต่างกันไปในแต่ละระบบ และไม่สัมพันธ์กับระยะเวลาของเกมวนซ้ำ ดังนั้นเกมเพลย์จึงอาจแตกต่างกันไปในแต่ละระบบ เพื่อสร้างประสบการณ์ที่สอดคล้องกัน สิ่งสำคัญคือต้องผสานรวมการตรวจหาเหตุการณ์แป้นพิมพ์เข้ากับ Game Loop อย่างใกล้ชิด

ข่าวดีคือเราได้รวม Wrapper ของ JS แบบ 16 บรรทัดไว้ให้สามารถใช้การค้นหาเหตุการณ์ได้ คีย์นี้มีชื่อว่า key_status.js และคุณค้นหาสถานะของคีย์ได้ทุกเมื่อด้วยการตรวจสอบ keydown.left เป็นต้น

ตอนนี้เราสามารถค้นหาว่าแป้นไม่ทำงานหรือไม่ เราจึงใช้วิธีการอัปเดตแบบง่ายๆ นี้เพื่อย้ายโปรแกรมเล่น

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

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

ทดลองได้เลย

คุณอาจพบว่าสามารถย้ายโปรแกรมเล่นออกจากหน้าจอได้ มาบีบตำแหน่งของผู้เล่นให้อยู่ภายในขอบเขตที่กำหนดกัน นอกจากนี้ โปรแกรมเล่นวิดีโอยังดูค่อนข้างช้า ดังนั้นมาเพิ่มความเร็วกันเถอะ

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

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

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

การเพิ่มอินพุตก็ทำได้ง่ายๆ งั้นมาเพิ่มกระสุนกันล่ะ

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...
};

การเพิ่มออบเจ็กต์เกมอีก

กระสุน

คราวนี้ลองเพิ่มกระสุนจริงกัน ก่อนอื่น เราต้องมีคอลเล็กชันเพื่อเก็บข้อมูลทั้งหมดไว้ที่

var playerBullets = [];

ต่อไป เราต้องใช้ตัวสร้างเพื่อสร้างอินสแตนซ์หัวข้อย่อย

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;
}

เมื่อผู้เล่นยิง เราจึงควรสร้างสัญลักษณ์แสดงหัวข้อย่อย และเพิ่มลงในชุดสัญลักษณ์แสดงหัวข้อย่อย

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
  };
};

ตอนนี้เราต้องเพิ่มการอัปเดตหัวข้อย่อยในฟังก์ชันขั้นตอนการอัปเดต เรากรองรายการหัวข้อย่อยให้แสดงเฉพาะหัวข้อย่อยที่ใช้งานอยู่เท่านั้น เพื่อป้องกันไม่ให้การรวบรวมหัวข้อย่อยเต็มโดยไม่กําหนด และยังช่วยให้เรานำกระสุนที่ชนกับศัตรูออกได้

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

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

ขั้นตอนสุดท้ายคือการวาดหัวข้อย่อย

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

ศัตรู

ตอนนี้ถึงเวลาเพิ่มศัตรูด้วยวิธีเดียวกับการเพิ่มหัวข้อย่อยแล้ว

  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();
  });
}

กำลังโหลดและวาดภาพ

มันเจ๋งมากถ้าได้เห็นกล่องที่บินไปมาแล้ว แต่การมีภาพสำหรับกล่องเหล่านี้คงจะเจ๋งกว่านั้นอีก การโหลดและวาดภาพบนผืนผ้าใบมักจะเป็นประสบการณ์ที่น้ำตาไหล เพื่อป้องกันความเจ็บปวดและความทุกข์ทรมานนั้น เราจึงใช้ชั้นเรียนยูทิลิตีแบบง่ายๆ

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);
  };

  ...
}

การตรวจจับการชน

เรามีดีลทั้งหมดนี้ลอยไปรอบๆ หน้าจอ แต่ก็ไม่ได้โต้ตอบซึ่งกันและกัน ถ้าจะให้ทุกอย่างรู้ว่าจะระเบิดเมื่อใด เราจะต้องเพิ่มการตรวจจับการชนกัน

มาใช้อัลกอริทึมการตรวจจับการชนกันแบบสี่เหลี่ยมง่ายๆ กัน

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;
}

มีข้อขัดข้อง 2 ประการที่เราต้องการตรวจสอบดังนี้

  1. หัวข้อย่อยของผู้เล่น => เรือศัตรู
  2. ผู้เล่น => เรือศัตรู

มาดูวิธีจัดการการชนกันซึ่งเราเรียกใช้จากเมธอดการอัปเดตกัน

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();
}

ตอนนี้เราต้องเพิ่มวิธีการระเบิดลงในผู้เล่นและศัตรู การดำเนินการนี้จะแจ้งให้นำออกและเพิ่มการระเบิด

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
};

เสียง

เพื่อเพิ่มเอฟเฟกต์เสียงหวานๆ เพื่อมอบประสบการณ์ที่สมบูรณ์ในการรับชม ฟังเหมือนกับรูปภาพ อาจไม่ใช่เรื่องง่ายนักเมื่อใช้ใน HTML5 แต่ด้วยสูตร Sound.js ที่ไม่ซับซ้อนของเรา ทำให้เสียงมีความเรียบง่ายสุดๆ

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

function Enemy(I) {
  ...

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

แม้ว่า API นี้จะปราศจากช่องโหว่ แต่การเพิ่มเสียงเป็นวิธีที่รวดเร็วที่สุดในการทำให้แอปพลิเคชันของคุณขัดข้อง การที่เสียงถูกตัดหรือเอาแท็บทั้งหมดของเบราว์เซอร์ออกนั้นไม่ใช่เรื่องแปลก ดังนั้นเตรียมเนื้อเยื่อของคุณให้พร้อม

คำอำลา

นี่เป็นการสาธิตเกมการทำงานแบบสมบูรณ์ คุณดาวน์โหลดซอร์สโค้ดเป็นรหัสไปรษณีย์ได้ด้วย

หวังว่าคุณจะสนุกกับการเรียนรู้พื้นฐานในการสร้างเกมง่ายๆ ด้วย JavaScript และ HTML5 การเขียนโปรแกรมที่ระดับ Abstraction ที่เหมาะสมช่วยให้เราพบเจอกับส่วนที่ยากกว่าของ API รวมถึงปรับตัวเมื่อเผชิญกับการเปลี่ยนแปลงในอนาคต

รายการอ้างอิง