กรณีศึกษา - สังหาร! สนามกีฬา

เกริ่นนำ

ในเดือนมิถุนายน 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
ซ้าย: เพิ่มชิ้นงานที่สมบูรณ์แบบแบบพิกเซลเป็น 2 เท่าใน Photoshop ขวา: การปรับขนาด CSS ทำให้เอฟเฟ็กต์เบลอ

นี่เป็นตัวทำลายดีลของเกมเพราะพิกเซลแต่ละพิกเซลมีความสำคัญมาก แต่หากคุณจำเป็นต้องปรับขนาด Canvas และการลดรอยหยักที่เหมาะกับโปรเจ็กต์ของคุณ คุณอาจพิจารณาวิธีนี้ด้วยเหตุผลด้านประสิทธิภาพ

เคล็ดลับสนุกๆ เกี่ยวกับภาพพิมพ์แคนวาส

เราทราบดีว่า <canvas> คือความฮอตใหม่ แต่บางครั้งนักพัฒนาซอฟต์แวร์ก็ยังคงแนะนำให้ใช้ DOM ถ้าอยากรู้ว่าจะใช้แอปไหนดี ก็มาดูตัวอย่างวิธีที่ <canvas> ช่วยประหยัดเวลาและพลังงานให้กับเราได้มากมาย

เมื่อศัตรูถูกโจมตีใน Onslaught! สนามกีฬา จะกะพริบเป็นสีแดงและ แสดงภาพเคลื่อนไหว "ความเจ็บปวด" สั้นๆ เราแสดงเฉพาะศัตรูเป็น "ความเจ็บปวด" ในทิศทางที่หันเหี่ยวลงเพื่อจำกัดจำนวนกราฟิกที่เราต้องสร้าง เกมนี้เหมาะสำหรับคุณในเกมและประหยัดเวลาในการสร้างสไปรท์ได้เยอะมาก แต่สำหรับบอสมอนสเตอร์ การเห็นสไปรท์ขนาดใหญ่ (ขนาด 64x64 พิกเซลขึ้นไป) สแนปจากซ้ายไปขวาหรือหงายขึ้นเพื่อหงายหน้าก้มลงเพื่อเฟรมที่เจ็บปวด

วิธีแก้ไขที่ชัดเจนคือการวางกรอบความเจ็บปวดสำหรับเจ้านายแต่ละคนไว้ในทั้ง 8 ทิศทาง แต่วิธีนี้จะใช้เวลามาก <canvas> ช่วยให้เราแก้ปัญหานี้ในโค้ดได้

ระวังตัวรับความเสียหายในการโจมตี! สนามกีฬา
คุณสร้างเอฟเฟกต์ที่น่าสนใจได้โดยใช้ Context.globalCompositeOperating

ก่อนอื่นเราวาดมอนสเตอร์ไปยัง "บัฟเฟอร์" <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 องศาไปพร้อมๆ กัน ผู้เล่นเกมฮาร์ดคอร์ ชื่นชมฟีเจอร์นี้ แต่ก็ให้ผลข้างเคียงจาก ผู้ใช้แทร็กแพดที่น่าหงุดหงิด

สังหาร! โมดัลการควบคุมอารีน่า (เลิกใช้งานแล้ว)
การควบคุมแบบเก่าหรือโมดัล "วิธีการเล่น" ใน Onslaught! สนามกีฬา

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

สังหาร! บทแนะนำการควบคุมอารีน่า
ผู้เล่นมักจะไม่สนใจการวางซ้อนบทแนะนำ แต่ชอบเล่นและสนุกมากกว่า

เรายังโชคดีที่มีแฟนๆ ในยุโรปบางคน แต่เราได้รับความไม่พอใจจากแฟนๆ ว่าอาจไม่มีแป้นพิมพ์ 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 ในเว็บ

ALT_TEXT_HERE
ระบบจะบันทึกคะแนนสูงสุด รวมถึงตำแหน่งของคุณในเกมหลังจากเอาชนะบอสแต่ละคน

เราตัดสินใจใช้ 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

สังหาร! สนามประลองที่มีโลโก้ HTML5 ซ่อนอยู่
คุณสามารถรับโล่ HTML5 ได้โดยพิมพ์ "html5" เวลาเล่น Onslaught! สนามกีฬา