การจัดการชิ้นงานง่ายๆ สําหรับเกม HTML5

บทนำ

HTML5 มี API ที่มีประโยชน์มากมายสําหรับการสร้างเว็บแอปพลิเคชันที่ทันสมัย ตอบสนองได้รวดเร็ว และมีประสิทธิภาพในเบราว์เซอร์ นี่เป็นวิธีที่ดี แต่คุณอยากสร้างและเล่นเกมจริงๆ แต่โชคดีที่ HTML5 ได้เปิดศักราชใหม่ในการพัฒนาเกมที่ใช้ API เช่น Canvas และเครื่องมือ JavaScript ที่มีประสิทธิภาพเพื่อแสดงเกมในเบราว์เซอร์โดยตรงโดยไม่ต้องใช้ปลั๊กอิน

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

ปัญหา

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

วิธีพื้นฐานในการโหลดรูปภาพในเว็บเบราว์เซอร์แบบเป็นโปรแกรมคือโค้ดต่อไปนี้

var image = new Image();
image.addEventListener("success", function(e) {
  // do stuff with the image
});
image.src = "/some/image.png";

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

โซลูชัน

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

เครื่องมือจัดการชิ้นงานแบบง่ายของเรามีข้อกำหนดต่อไปนี้

  • จัดคิวการดาวน์โหลด
  • เริ่มดาวน์โหลด
  • ติดตามความสําเร็จและความล้มเหลว
  • สัญญาณเมื่อทุกอย่างเสร็จสิ้น
  • ดึงข้อมูลชิ้นงานได้ง่ายๆ

การจัดคิว

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

โค้ดสำหรับคอนสตรัคเตอร์และการจัดคิวมีลักษณะดังนี้

function AssetManager() {
  this.downloadQueue = [];
}

AssetManager.prototype.queueDownload = function(path) {
    this.downloadQueue.push(path);
}

เริ่มดาวน์โหลด

หลังจากจัดคิวชิ้นงานทั้งหมดที่จะดาวน์โหลดแล้ว คุณสามารถขอให้เครื่องมือจัดการชิ้นงานเริ่มดาวน์โหลดทุกอย่างได้

โชคดีที่เว็บเบราว์เซอร์สามารถดาวน์โหลดแบบขนานได้ โดยปกติจะมีการเชื่อมต่อสูงสุด 4 รายการต่อโฮสต์ วิธีหนึ่งในการเร่งความเร็วการดาวน์โหลดชิ้นงานคือการใช้ชื่อโดเมนที่หลากหลายสำหรับการโฮสต์ชิ้นงาน เช่น แทนที่จะแสดงทุกอย่างจาก assets.example.com ให้ลองใช้ assets1.example.com, assets2.example.com, assets3.example.com และอื่นๆ แม้ว่าชื่อโดเมนแต่ละรายการจะเป็น CNAME ของเว็บเซิร์ฟเวอร์เดียวกัน แต่เว็บเบราว์เซอร์จะถือว่าโดเมนเหล่านั้นเป็นเซิร์ฟเวอร์แยกต่างหากและเพิ่มจำนวนการเชื่อมต่อที่ใช้สำหรับการดาวน์โหลดชิ้นงาน ดูข้อมูลเพิ่มเติมเกี่ยวกับเทคนิคนี้ได้จากแยกคอมโพเนนต์ไปยังโดเมนต่างๆ ในแนวทางปฏิบัติแนะนำในการเร่งความเร็วเว็บไซต์

เมธอดสำหรับการเริ่มต้นการดาวน์โหลดของเราเรียกว่า downloadAll() เราจะค่อยๆ เพิ่มฟีเจอร์นี้ไปเรื่อยๆ ในระหว่างนี้ นี่เป็นตรรกะแรกสำหรับการเริ่มการดาวน์โหลด

AssetManager.prototype.downloadAll = function() {
    for (var i = 0; i < this.downloadQueue.length; i++) {
        var path = this.downloadQueue[i];
        var img = new Image();
        var that = this;
        img.addEventListener("load", function() {
            // coming soon
        }, false);
        img.src = path;
    }
}

ดังที่คุณเห็นในโค้ดด้านบน downloadAll() เพียงแค่วนผ่าน downloadQueue และสร้างออบเจ็กต์ Image ใหม่ ระบบจะเพิ่ม Listener เหตุการณ์สําหรับเหตุการณ์การโหลดและตั้งค่า src ของรูปภาพ ซึ่งจะทริกเกอร์การดาวน์โหลดจริง

วิธีนี้จะช่วยให้คุณเริ่มการดาวน์โหลดได้

การติดตามความสําเร็จและการไม่สําเร็จ

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

AssetManager.prototype.downloadAll = function(downloadCallback) {
  for (var i = 0; i < this.downloadQueue.length; i++) {
    var path = this.downloadQueue[i];
    var img = new Image();
    var that = this;
    img.addEventListener("load", function() {
        // coming soon
    }, false);
    img.addEventListener("error", function() {
        // coming soon
    }, false);
    img.src = path;
  }
}

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

ก่อนอื่น เราจะเพิ่มตัวนับลงในออบเจ็กต์ในคอนสตรัคเตอร์ ซึ่งตอนนี้มีลักษณะดังนี้

function AssetManager() {
<span class="highlight">    this.successCount = 0;
    this.errorCount = 0;</span>
    this.downloadQueue = [];
}

ถัดไป ให้เพิ่มตัวนับใน Listener เหตุการณ์ ซึ่งตอนนี้มีลักษณะดังนี้

img.addEventListener("load", function() {
    <span class="highlight">that.successCount += 1;</span>
}, false);
img.addEventListener("error", function() {
    <span class="highlight">that.errorCount += 1;</span>
}, false);

ตอนนี้เครื่องมือจัดการชิ้นงานจะติดตามทั้งชิ้นงานที่โหลดสําเร็จและชิ้นงานที่โหลดไม่สําเร็จ

การส่งสัญญาณเมื่อเสร็จสิ้น

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

ผู้จัดการชิ้นงานต้องทราบก่อนว่าชิ้นงานทุกรายการเสร็จแล้ว เราจะเพิ่มเมธอด isDone ดังนี้

AssetManager.prototype.isDone = function() {
    return (this.downloadQueue.length == this.successCount + this.errorCount);
}

เครื่องมือจัดการชิ้นงานจะทราบได้ว่าชิ้นงานทุกรายการเสร็จสมบูรณ์หรือมีข้อผิดพลาดบางอย่างหรือไม่โดยการเปรียบเทียบ successCount + errorCount กับขนาดของ downloadQueue

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

img.addEventListener("load", function() {
    console.log(this.src + ' is loaded');
    that.successCount += 1;
    if (that.isDone()) {
        // ???
    }
}, false);
img.addEventListener("error", function() {
    that.errorCount += 1;
if (that.isDone()) {
        // ???
    }
}, false);

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

หากเครื่องมือจัดการชิ้นงานดาวน์โหลดชิ้นงานทั้งหมดเสร็จแล้ว เราจะเรียกใช้เมธอดการเรียกกลับ มาเปลี่ยน downloadAll() และเพิ่มพารามิเตอร์สําหรับการเรียกกลับกัน

AssetManager.prototype.downloadAll = function(downloadCallback) {
    ...

เราจะเรียกใช้เมธอด downloadCallback ภายใน Listener เหตุการณ์ ดังนี้

img.addEventListener("load", function() {
    that.successCount += 1;
    if (that.isDone()) {
        downloadCallback();
    }
}, false);
img.addEventListener("error", function() {
    that.errorCount += 1;
    if (that.isDone()) {
        downloadCallback();
    }
}, false);

ในที่สุดผู้จัดการชิ้นงานก็พร้อมสำหรับข้อกําหนดสุดท้ายแล้ว

ดึงข้อมูลชิ้นงานได้ง่ายๆ

เมื่อได้รับสัญญาณว่าเริ่มเกมได้ เกมจะเริ่มแสดงผลภาพ ผู้จัดการเนื้อหาไม่เพียงมีหน้าที่รับผิดชอบในการดาวน์โหลดและติดตามเนื้อหาเท่านั้น แต่ยังต้องจัดหาเนื้อหาให้กับเกมด้วย

ข้อกำหนดสุดท้ายของเราบอกเป็นนัยถึงเมธอด getAsset ประเภทหนึ่ง ดังนั้นเราจะเพิ่มเมธอดนี้เลย

AssetManager.prototype.getAsset = function(path) {
    return this.cache[path];
}

ออบเจ็กต์แคชนี้จะเริ่มต้นในคอนสตรัคเตอร์ ซึ่งตอนนี้มีลักษณะดังนี้

function AssetManager() {
    this.successCount = 0;
    this.errorCount = 0;
    this.cache = {};
    this.downloadQueue = [];
}

ระบบจะสร้างแคชที่ส่วนท้ายของ downloadAll() ดังที่แสดงด้านล่าง

AssetManager.prototype.downloadAll = function(downloadCallback) {
  ...
      img.addEventListener("error", function() {
          that.errorCount += 1;
          if (that.isDone()) {
              downloadCallback();
          }
      }, false);
      img.src = path;
      <span class="highlight">this.cache[path] = img;</span>
  }
}

โบนัส: การแก้ไขข้อบกพร่อง

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

คุณรองรับสถานการณ์นี้ได้โดยการเพิ่มโค้ดต่อไปนี้ลงใน downloadAll()

AssetManager.prototype.downloadAll = function(downloadCallback) {
    if (this.downloadQueue.length === 0) {
      downloadCallback();
  }
 ...

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

ตัวอย่างการใช้งาน

การใช้เครื่องมือจัดการชิ้นงานนี้ในเกม HTML5 นั้นค่อนข้างตรงไปตรงมา วิธีพื้นฐานที่สุดในการใช้คลังมีดังนี้

var ASSET_MANAGER = new AssetManager();

ASSET_MANAGER.queueDownload('img/earth.png');

ASSET_MANAGER.downloadAll(function() {
    var sprite = ASSET_MANAGER.getAsset('img/earth.png');
    ctx.drawImage(sprite, x - sprite.width/2, y - sprite.height/2);
});

โค้ดด้านบนแสดงถึงสิ่งต่อไปนี้

  1. สร้างเครื่องมือจัดการชิ้นงานใหม่
  2. จัดคิวชิ้นงานที่รอดาวน์โหลด
  3. เริ่มการดาวน์โหลดด้วย downloadAll()
  4. ส่งสัญญาณเมื่อชิ้นงานพร้อมใช้งานโดยเรียกใช้ฟังก์ชันการเรียกกลับ
  5. เรียกข้อมูลชิ้นงานด้วย getAsset()

สิ่งที่ยังพัฒนาได้อีก

แน่นอนว่าคุณจะต้องใช้เครื่องมือจัดการชิ้นงานแบบง่ายนี้ต่อไปไม่ได้เมื่อสร้างเกม แต่เราหวังว่าเครื่องมือนี้จะเป็นจุดเริ่มต้นที่ดี ฟีเจอร์ในอนาคตอาจรวมถึงสิ่งต่อไปนี้

  • ระบุชิ้นงานที่มีข้อผิดพลาด
  • การเรียกกลับเพื่อระบุความคืบหน้า
  • การดึงข้อมูลชิ้นงานจาก File System API

โปรดโพสต์การปรับปรุง การแยกไปใช้งาน และลิงก์ไปยังโค้ดในความคิดเห็นด้านล่าง

แหล่งที่มาแบบเต็ม

แหล่งที่มาของเครื่องมือจัดการชิ้นงานนี้และเกมที่นำมาสร้างเป็นเครื่องมือจัดการชิ้นงานเป็นโอเพนซอร์สภายใต้สัญญาอนุญาต Apache และดูได้ในบัญชี GitHub ของ Bad Aliens เกม Bad Aliens เล่นได้ในเบราว์เซอร์ที่รองรับ HTML5 เกมนี้เป็นหัวข้อในการบรรยายของฉันใน Google IO ชื่อ Super Browser 2 Turbo HD Remix: ข้อมูลเบื้องต้นเกี่ยวกับการพัฒนาเกม HTML5 (สไลด์, วิดีโอ)

สรุป

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