เกริ่นนำ
ถ้าคุณอยากสร้างเกมโดยใช้ 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 ประการที่เราต้องการตรวจสอบดังนี้
- หัวข้อย่อยของผู้เล่น => เรือศัตรู
- ผู้เล่น => เรือศัตรู
มาดูวิธีจัดการการชนกันซึ่งเราเรียกใช้จากเมธอดการอัปเดตกัน
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 รวมถึงปรับตัวเมื่อเผชิญกับการเปลี่ยนแปลงในอนาคต