เกริ่นนำ
ในเดือนมิถุนายน 2010 เราสังเกตเห็นว่า Boing Boing ที่เผยแพร่ในท้องถิ่นมีการแข่งขันพัฒนาเกม
เราเห็นว่านี่เป็นวิธีที่ดีอย่างยิ่งในการสร้างเกมที่เร็วและเรียบง่ายใน JavaScript และ <canvas>
เราจึงเริ่มนำมาใช้ หลังจบการแข่งขัน เรายังมีไอเดีย
อีกมากและอยากทำให้สิ่งที่เราเริ่มไว้ทำให้เสร็จ นี่คือกรณีศึกษาของผลลัพธ์ ซึ่งเป็นเกมเล็กๆ ที่ชื่อว่า Onslaught! สนามกีฬา
ภาพย้อนยุคแบบพิกเซล
การทำให้เกมของเรามีรูปลักษณ์และความรู้สึกเหมือนเกม Nintendo Entertainment System แบบย้อนยุคโดยใช้สถานที่จัดการแข่งขันต้องพัฒนาเกมที่อิงตามชิปจูน เกมส่วนใหญ่ไม่มีข้อกำหนดนี้ แต่ยังคงเป็นสไตล์ศิลปะที่ใช้กันทั่วไป (โดยเฉพาะในบรรดานักพัฒนาเกมอินดี้) เนื่องจากสร้างชิ้นงานได้ง่ายและเป็นธรรมชาติสำหรับเกมเมอร์ที่ย้อนวันวาน
เนื่องจากสไปรท์เหล่านี้มีขนาดเล็กเพียงใด เราจึงตัดสินใจที่จะเพิ่มพิกเซลเป็น 2 เท่า ซึ่งหมายความว่าตอนนี้สไปรท์ขนาด 16x16 จะเป็นขนาด 32x32 พิกเซลเป็นต้น ตั้งแต่แรกเริ่ม เราเพิ่ม 2 เท่าเกี่ยวกับการสร้างเนื้อหา แทนที่จะทำให้เบราว์เซอร์ทำงานหนัก วิธีนี้ใช้ง่ายกว่า แต่มีข้อได้เปรียบด้านรูปลักษณ์ที่ชัดเจน
เราจะพิจารณาสถานการณ์ต่อไปนี้
<style>
canvas {
width: 640px;
height: 320px;
}
</style>
<canvas width="320" height="240">
Sorry, your browser is not supported.
</canvas>
เมธอดนี้จะประกอบด้วยสไปรท์ขนาด 1x1 แทนที่จะเพิ่มเป็น 2 เท่าในด้านการสร้างชิ้นงาน จากนั้น CSS จะเข้ามาแทนที่และปรับขนาด Canvas การเปรียบเทียบของเราเผยให้เห็นว่าวิธีนี้เร็วเป็น 2 เท่าของการแสดงผลรูปภาพขนาดใหญ่ขึ้น (2 เท่า) แต่น่าเสียดายที่การปรับขนาด CSS นั้นรวมถึงการลดรอยหยัก ซึ่งเป็นสิ่งที่เราไม่มีวิธีป้องกันให้
นี่เป็นตัวทำลายดีลของเกมเพราะพิกเซลแต่ละพิกเซลมีความสำคัญมาก แต่หากคุณจำเป็นต้องปรับขนาด Canvas และการลดรอยหยักที่เหมาะกับโปรเจ็กต์ของคุณ คุณอาจพิจารณาวิธีนี้ด้วยเหตุผลด้านประสิทธิภาพ
เคล็ดลับสนุกๆ เกี่ยวกับภาพพิมพ์แคนวาส
เราทราบดีว่า <canvas>
คือความฮอตใหม่ แต่บางครั้งนักพัฒนาซอฟต์แวร์ก็ยังคงแนะนำให้ใช้ DOM ถ้าอยากรู้ว่าจะใช้แอปไหนดี ก็มาดูตัวอย่างวิธีที่ <canvas>
ช่วยประหยัดเวลาและพลังงานให้กับเราได้มากมาย
เมื่อศัตรูถูกโจมตีใน Onslaught! สนามกีฬา จะกะพริบเป็นสีแดงและ แสดงภาพเคลื่อนไหว "ความเจ็บปวด" สั้นๆ เราแสดงเฉพาะศัตรูเป็น "ความเจ็บปวด" ในทิศทางที่หันเหี่ยวลงเพื่อจำกัดจำนวนกราฟิกที่เราต้องสร้าง เกมนี้เหมาะสำหรับคุณในเกมและประหยัดเวลาในการสร้างสไปรท์ได้เยอะมาก แต่สำหรับบอสมอนสเตอร์ การเห็นสไปรท์ขนาดใหญ่ (ขนาด 64x64 พิกเซลขึ้นไป) สแนปจากซ้ายไปขวาหรือหงายขึ้นเพื่อหงายหน้าก้มลงเพื่อเฟรมที่เจ็บปวด
วิธีแก้ไขที่ชัดเจนคือการวางกรอบความเจ็บปวดสำหรับเจ้านายแต่ละคนไว้ในทั้ง 8 ทิศทาง แต่วิธีนี้จะใช้เวลามาก <canvas>
ช่วยให้เราแก้ปัญหานี้ในโค้ดได้
ก่อนอื่นเราวาดมอนสเตอร์ไปยัง "บัฟเฟอร์" <canvas>
ที่ซ่อนอยู่
วางซ้อนด้วยสีแดง จากนั้นแสดงผลผลลัพธ์กลับไปยังหน้าจอ โค้ดจะมีลักษณะดังนี้
// Get the "buffer" canvas (that isn't visible to the user)
var bufferCanvas = document.getElementById("buffer");
var buffer = bufferCanvas.getContext("2d");
// Draw your image on the buffer
buffer.drawImage(image, 0, 0);
// Draw a rectangle over the image using a nice translucent overlay
buffer.save();
buffer.globalCompositeOperation = "source-in";
buffer.fillStyle = "rgba(186, 51, 35, 0.6)"; // red
buffer.fillRect(0, 0, image.width, image.height);
buffer.restore();
// Copy the buffer onto the visible canvas
document.getElementById("stage").getContext("2d").drawImage(bufferCanvas, x, y);
Game Loop
การพัฒนาเกมมีความแตกต่างที่โดดเด่นจากการพัฒนาเว็บ ในสแต็กเว็บ การแสดงความรู้สึกต่อเหตุการณ์ที่เกิดขึ้นผ่าน Listener เหตุการณ์นั้นถือเป็นเรื่องปกติ ดังนั้นโค้ดการเริ่มต้นจึงอาจดำเนินการใดๆ นอกจากการคอยฟังเหตุการณ์อินพุต ตรรกะของเกมจะแตกต่างกันไปเนื่องจากต้องอัปเดตตัวเองอยู่ตลอดเวลา ตัวอย่างเช่น หากผู้เล่นไม่ขยับตัว ก็ไม่ควรหยุดก็อบลินจะกำจัดเขา!
ต่อไปนี้คือตัวอย่างของ Game Loop
function main () {
handleInput();
update();
render();
};
setInterval(main, 1);
ความแตกต่างที่สำคัญอย่างแรกคือ ฟังก์ชัน handleInput
ไม่ได้ทำอะไรในทันที หากผู้ใช้กดแป้นในเว็บแอปทั่วไป ก็ควรดำเนินการตามที่ต้องการทันที แต่ในเกม เรื่องต่างๆ ต้องเกิดขึ้นตามลำดับเวลาจึงจะเล่นได้ถูกต้อง
window.addEventListener("mousedown", function(e) {
// A mouse click means the players wants to attack.
// We don't actually do that yet, but instead tell the rest
// of the program about the request.
buttonStates[e.button] = true;
}, false);
function handleInput() {
// Here is where we respond to the click
if (buttonStates[LEFT_BUTTON]) {
player.attacking = true;
delete buttonStates[LEFT_BUTTON];
}
};
ตอนนี้เราทราบเกี่ยวกับอินพุตแล้วและพิจารณาในฟังก์ชัน update
ได้เพราะรู้ว่าจะปฏิบัติตามกฎอื่นๆ ของเกม
function update() {
// Check for collisions, states, whatever else is needed
// If after that the player can still attack, do it!
if (player.attacking && player.canAttack()) {
player.attack();
}
};
สุดท้ายนี้ เมื่อคำนวณทุกอย่างเรียบร้อยแล้ว ก็ได้เวลาดึงหน้าจอขึ้นมาอีกครั้ง
ในพื้นที่ DOM เบราว์เซอร์จะจัดการกับการยกฟีดขึ้น แต่เมื่อใช้ <canvas>
คุณต้องเขียนซ้ำด้วยตนเองทุกครั้งที่มีบางอย่างเกิดขึ้น (ซึ่งปกติก็คือทุกเฟรมเลย)
function render() {
// First erase everything, something like:
context.clearRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
// Draw the player (and whatever else you need)
context.drawImage(
player.getImage(),
player.x, player.y
);
};
การสร้างโมเดลตามเวลา
การประมาณตามเวลาเป็นแนวคิดของการย้ายสไปรท์ตามระยะเวลาที่ผ่านไปนับตั้งแต่การอัปเดตเฟรมครั้งล่าสุด เทคนิคนี้ช่วยให้เกมทำงานได้เร็วที่สุดและยังทำให้สไปรท์เคลื่อนที่ด้วยความเร็วที่สม่ำเสมอ
ในการใช้โมเดลตามเวลา เราต้องเก็บเวลาที่ผ่านไปนับจากที่มีการวาดเฟรมสุดท้าย เราจะต้องเพิ่มฟังก์ชัน update()
ของ Game Loop เพื่อติดตามสิ่งนี้
function update() {
// NOTE: You'll need to initially seed this.lastUpdate
// with the current time when your game loop starts
// this.lastUpdate = Date.now();
// Calculate elapsed time since last frame
var now = Date.now();
var elapsed = (now - this.lastUpdate);
this.lastUpdate = now;
// Do stuff with elapsed
};
ตอนนี้เราก็จะมีเวลาที่ผ่านไป เราสามารถคำนวณได้ว่าสไปรท์หนึ่งๆ ควรเคลื่อนที่แต่ละเฟรมไปไกลแค่ไหน ก่อนอื่น เราต้องติดตามดูสิ่งต่างๆ ในสไปรท์ เช่น ตำแหน่งปัจจุบัน ความเร็ว และทิศทาง
var Sprite = function() {
// The sprite's position relative to the top left of the game world
this.position = {x: 0, y: 0};
// The sprite's direction. A positive x value indicates moving to the right
this.direction = {x: 1, y: 0};
// How many pixels the sprite moves per second
this.speed = 50;
};
เมื่อคำนึงถึงตัวแปรเหล่านี้แล้ว เราจะย้ายอินสแตนซ์ของคลาสสไปรท์ข้างต้นโดยใช้การประมาณตามเวลา
// Determine how far this sprite will move this frame
var distance = (sprite.speed / 1000) * elapsed;
// Apply the movement distance to the sprite's current position
// taking into account its direction
sprite.position.x += (distance * sprite.direction.x);
sprite.position.y += (distance * sprite.direction.y);
โปรดทราบว่าค่า direction.x
และ direction.y
ควรได้รับการปรับให้เป็นมาตรฐาน ซึ่งหมายความว่าควรอยู่ระหว่าง -1
และ 1
เสมอ
การควบคุม
การควบคุมอาจเป็นอุปสรรคที่ใหญ่ที่สุดในการพัฒนา Onslaught! สนามกีฬา เดโมแรกสนับสนุนแป้นพิมพ์เท่านั้น ผู้เล่นย้ายตัวละครหลักไปรอบๆ หน้าจอด้วยแป้นลูกศรและยิงไปในทิศทางที่เขาหันหน้าไปใกล้กับแป้นเว้นวรรค แม้ว่าจะเข้าใจและเข้าใจยาก แต่วิธีนี้ทำให้เกมนี้เล่นในระดับที่ยากขึ้นจนแทบจะเล่นไม่ได้เลย การที่มีศัตรูและกระสุนปืนยิงใส่ผู้เล่นหลายสิบตัวอยู่ตลอดเวลา จำเป็นที่จะต้องสามารถปะทะกับผู้ร้ายขณะยิงในทิศทางใดก็ได้
ในการเปรียบเทียบกับเกมที่คล้ายกันในแนวเกม เราได้เพิ่มการรองรับเมาส์เพื่อควบคุมเป้าเล็ง ซึ่งตัวละครจะใช้เพื่อเล็งการโจมตี ตัวละครยังคงเคลื่อนที่ด้วยแป้นพิมพ์ได้ แต่หลังการเปลี่ยนแปลงนี้ เขาสามารถยิงได้ในทิศทางเต็ม 360 องศาไปพร้อมๆ กัน ผู้เล่นเกมฮาร์ดคอร์ ชื่นชมฟีเจอร์นี้ แต่ก็ให้ผลข้างเคียงจาก ผู้ใช้แทร็กแพดที่น่าหงุดหงิด
เพื่ออำนวยความสะดวกแก่ผู้ใช้แทร็กแพด เราได้นำการควบคุมปุ่มลูกศรกลับมาซึ่งครั้งนี้ เพื่อให้สามารถเริ่มทำงานในทิศทางที่กดได้ แม้เราคิดว่าเราตอบสนองความต้องการ ของผู้เล่นทุกประเภท แต่เราก็ไม่ได้เจตนาทำให้เกมของเราซับซ้อนเกินไปโดยไม่รู้ตัว ที่น่าประหลาดใจก็คือ เราได้ข่าวในภายหลังว่าผู้เล่นบางคนไม่ทราบ การใช้ตัวควบคุมเมาส์ (หรือแป้นพิมพ์!) สำหรับการโจมตี แม้จะไม่มีโมดัลบทแนะนำที่โดยส่วนใหญ่แล้วจะไม่สนใจก็ตาม
เรายังโชคดีที่มีแฟนๆ ในยุโรปบางคน แต่เราได้รับความไม่พอใจจากแฟนๆ ว่าอาจไม่มีแป้นพิมพ์ QWERTY แบบปกติและไม่สามารถใช้ แป้น WASD เพื่อเคลื่อนที่ตามทิศทางได้ ผู้เล่นที่ถนัดซ้ายก็แสดงข้อร้องเรียนที่คล้ายกัน
เมื่อคุณใช้รูปแบบการควบคุมที่ซับซ้อนนี้ ก็จะทำให้มีปัญหาในการเล่นบนอุปกรณ์เคลื่อนที่ด้วย อันที่จริง คำขอหนึ่งที่เราพบบ่อยที่สุดคือ ให้สร้าง Onslaught! Arena มีให้บริการใน Android, iPad และ อุปกรณ์แบบสัมผัสอื่นๆ (ที่ไม่มีแป้นพิมพ์) จุดแข็งอย่างหนึ่งของ HTML5 คือความสามารถในการพกพา ดังนั้นการนำเกมไปยังอุปกรณ์เหล่านี้เป็นเรื่องที่ทำได้อย่างแน่นอน เราแค่ต้องแก้ปัญหาต่างๆ มากมาย (ที่สำคัญที่สุดคือการควบคุมและประสิทธิภาพ)
เพื่อแก้ไขปัญหามากมายดังกล่าว เราจึงเริ่มเล่นด้วยวิธีเล่นเกมแบบอินพุตเดียว โดยจะเป็นการโต้ตอบผ่านเมาส์ (หรือการแตะ) เท่านั้น ผู้เล่นคลิกหรือแตะที่หน้าจอแล้วตัวละครหลักจะเดินไปยังบริเวณที่กดแล้ว และจะโจมตีผู้ร้ายที่อยู่ใกล้ที่สุดโดยอัตโนมัติ โค้ดจะมีลักษณะดังนี้
// Find the nearest hostile target (if any) to the player
var player = this.getPlayerObject();
var hostile = this.getNearestHostile(player);
if (hostile !== null) {
// Found one! Shoot in its direction
var shoot = hostile.boundingBox().center().subtract(
player.boundingBox().center()
).normalize();
}
// Move towards where the player clicked/touched
var move = this.targetReticle.position.clone().subtract(
player.boundingBox().center()
).normalize();
var distance = this.targetReticle.position.clone().subtract(
player.boundingBox().center()
).magnitude();
// Prevent jittering if the character is close enough
if (distance < 3) {
move.zero();
}
// Move the player
if ((move.x !== 0) || (move.y !== 0)) {
player.setDirection(move);
}
การนำปัจจัยเพิ่มเติมของการเล็งไปที่ศัตรูออกอาจช่วยให้เล่นเกมได้ง่ายขึ้นในบางสถานการณ์ แต่เราคิดว่าการทำให้ผู้เล่นเล่นเกมง่ายขึ้นมีข้อดีหลายข้อ เกิดกลยุทธ์อื่นๆ ขึ้น เช่น การวางตัวละครไว้ใกล้กับศัตรูตัวฉกาจเพื่อมุ่งเป้าไปยังศัตรู และความสามารถในการรองรับอุปกรณ์สัมผัสนั้นเป็นเรื่องที่ประเมินค่าไม่ได้
เสียง
ในบรรดาการควบคุมและประสิทธิภาพ ซึ่งเป็นหนึ่งในปัญหาที่ใหญ่ที่สุดของเราขณะพัฒนา
Onslaught! Arena เดิมคือแท็ก <audio>
ของ HTML5
บางทีสิ่งที่แย่ที่สุดก็คือเวลาในการตอบสนอง: ในเบราว์เซอร์เกือบทั้งหมดจะมีความล่าช้าระหว่างการเรียก .play()
และเสียงที่เล่นอยู่จริงๆ ซึ่งจะทำลายประสบการณ์ของเกมเมอร์ โดยเฉพาะเมื่อเล่นเกมที่ดำเนินเรื่องอย่างรวดเร็วแบบของเรา
ปัญหาอื่นๆ ได้แก่ เหตุการณ์ "คืบหน้า" ไม่เริ่มทำงาน ซึ่งอาจทำให้ขั้นตอนการโหลดเกมค้างโดยไม่มีกำหนด ด้วยเหตุผลเหล่านี้ เราจึงนำวิธีการที่เราเรียกว่า "แบบส่งต่อ" ซึ่งหาก Flash โหลดไม่สำเร็จ เราก็จะเปลี่ยนเป็นรูปแบบเสียง HTML5 โค้ดจะมีลักษณะดังนี้
/*
This example uses the SoundManager 2 library by Scott Schiller:
http://www.schillmania.com/projects/soundmanager2/
*/
// Default to sm2 (Flash)
var api = "sm2";
function initAudio (callback) {
switch (api) {
case "sm2":
soundManager.onerror = (function (init) {
return function () {
api = "html5";
init(callback);
};
}(arguments.callee));
break;
case "html5":
var audio = document.createElement("audio");
if (
audio
&& audio.canPlayType
&& audio.canPlayType("audio/mpeg;")
) {
callback();
} else {
// No audio support :(
}
break;
}
};
นอกจากนี้ ควรรองรับเบราว์เซอร์ที่ไม่เปิดไฟล์ MP3 (เช่น Mozilla Firefox) ด้วย ในกรณีนี้ ระบบจะตรวจจับการสนับสนุนและเปลี่ยนเป็น Ogg Vorbis ได้โดยใช้โค้ดดังนี้
/*
Note: you could instead use "new Audio()" here,
but the client will throw an error if it doesn't support Audio,
which makes using "document.createElement" a safer approach.
*/
var audio = document.createElement("audio");
if (audio && audio.canPlayType) {
if (!audio.canPlayType("audio/mpeg;")) {
// Here you know you CANNOT use .mp3 files
if (audio.canPlayType("audio/ogg; codecs=vorbis")) {
// Here you know you CAN use .ogg files
}
}
}
กำลังบันทึกข้อมูล
ยิงท่าอาร์เคดให้เต็มที่ไม่ได้ถ้าทำคะแนนสูงสุดไม่ได้ เรารู้ว่าจะต้องเก็บข้อมูลเกมบางส่วนไว้ไม่อยู่ และแม้ว่าเราจะใช้อะไรเก่าๆ อย่างคุกกี้ได้ เราก็ต้องการเจาะลึกเกี่ยวกับเทคโนโลยี HTML5 ใหม่สนุกๆ ตัวเลือกมากมาย ได้แก่ พื้นที่เก็บข้อมูลในเครื่อง พื้นที่เก็บข้อมูลเซสชัน และฐานข้อมูล SQL ในเว็บ
เราตัดสินใจใช้ localStorage
เนื่องจากเป็นโซลูชันใหม่ น่าประทับใจ และใช้งานง่าย รองรับการบันทึกคู่คีย์/ค่าพื้นฐานซึ่งเป็นเกมง่ายๆ ที่เราต้องใช้ ต่อไปนี้เป็นตัวอย่างวิธีใช้ที่ตรงไปตรงมา
if (typeof localStorage == "object") {
localStorage.setItem("foo", "bar");
localStorage.getItem("foo"); // Value is "bar"
localStorage.removeItem("foo");
localStorage.getItem("foo"); // Value is now null
}
มี "ข้อผิดพลาด" บางอย่างที่คุณควรทราบ ไม่ว่าคุณจะส่งอะไร ระบบจะจัดเก็บค่าเป็นสตริง ซึ่งอาจทำให้เกิดผลลัพธ์ที่ไม่คาดคิดดังนี้
localStorage.setItem("foo", false);
typeof localStorage.getItem("foo"); // Value is "false" (a string literal)
if (localStorage.getItem("foo")) {
// It's true!
}
// Don't pass objects into setItem
localStorage.setItem("bar", {"key": "value"});
localStorage.getItem("bar"); // Value is "[object Object]" (a string literal)
// JSON stringify and parse when dealing with localStorage
localStorage.setItem("json", JSON.stringify({"key": "value"}));
typeof localStorage.getItem("json"); // string
JSON.parse(localStorage.getItem("json")); // {"key": "value"}
สรุป
คุณสามารถใช้ HTML5 ได้อย่างยอดเยี่ยม การติดตั้งใช้งานส่วนใหญ่จะจัดการทุกอย่างที่นักพัฒนาเกม
ต้องการ ตั้งแต่กราฟิกไปจนถึงการบันทึกสถานะของเกม แม้จะมีปัญหาต่างๆ มากขึ้นเรื่อยๆ (เช่น ปัญหาเรื่องแท็ก <audio>
) นักพัฒนาซอฟต์แวร์เบราว์เซอร์ก็เริ่มดำเนินการอย่างรวดเร็วและมีสิ่งต่างๆ ที่ยอดเยี่ยมอยู่แล้วไม่ต่างไปจากเดิม แต่อนาคตดูสดใสสำหรับเกมที่สร้างด้วย HTML5