ניהול נכסים פשוט למשחקי 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 התמונות יהיו מוכנות? האם כולם נטענו בהצלחה? מתי המשחק אמור להתחיל בפועל?

הפתרון

נותנים למנהל הנכסים לטפל בהוספת הנכסים לתור ולדווח למשחק כשהכול מוכן. מנהל נכסים מספק דרך נוחה לבדוק את הסטטוס של הנכסים, ומאפשר להכליל את הלוגיקה של טעינת הנכסים ברשת.

מנהל הנכסים הפשוט שלנו כולל את הדרישות הבאות:

  • להוסיף הורדות לתור
  • להתחיל הורדות
  • מעקב אחר הצלחות וכישלונות
  • סימון כשהכול יסתיים
  • אחזור קל של נכסים

הוספה לתור

הדרישה הראשונה היא להוסיף הורדות לתור. בעזרת העיצוב הזה אפשר להצהיר על הנכסים הנחוצים בלי להוריד אותם בפועל. אפשר להשתמש באפשרות הזו, למשל, אם רוצים להצהיר על כל הנכסים של רמת משחק בקובץ תצורה.

הקוד של ה-constructor וההמתנה בתור נראה כך:

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 חדש. נוסף מאזין לאירועים של אירוע הטעינה וה-src של התמונה מוגדר, מה שמפעיל את ההורדה בפועל.

בשיטה הזו אפשר להתחיל את ההורדות.

מעקב אחר הצלחות וכישלונות

דרישות נוספות הן לעקוב גם אחרי ההצלחות וגם אחרי הכישלונות, כי לצערי לא תמיד הכל עובד בצורה מושלמת. עד כה, הקוד עוקב רק אחרי נכסים שהורדו בהצלחה. הוספת מאזין לאירועים לאירוע השגיאה תאפשר לכם לתעד תרחישים של הצלחה ושל כשל.

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

מנהל הנכסים שלנו צריך לדעת כמה פעמים הצלחנו וכמה פעמים נכשלת, אחרת הוא לעולם לא ידע מתי המשחק יוכל להתחיל.

קודם כל, נוסיף את המונים לאובייקט ב-constructor, שנראה עכשיו כך:

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

בשלב הבא, מגדילים את המספרים בספירות של פונקציות ה-event 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, מנהל הנכסים יכול לדעת אם כל הנכסים הסתיימו בהצלחה או אם הייתה שגיאה כלשהי.

כמובן, לדעת אם זה בוצע זה רק חצי מהמאבק. מנהל הנכסים צריך לבדוק גם את השיטה הזו. נוסיף את הבדיקה הזו בשני פונקציות הטיפול באירועים, כפי שמוצג בקוד שבהמשך:

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

אחרי העלייה בספירה של המונים, נבדוק אם זה היה הנכס האחרון בתור. אם מערכת ניהול הנכסים סיימה את ההורדה, מה עלינו לעשות בדיוק?

אם מנהל הנכסים סיים להוריד את כל הנכסים, אנחנו נתקשר לשיטת קריאה חוזרת (callback), כמובן. נשנה את downloadAll() ונוסיף פרמטר לקריאה החוזרת:

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

נפעיל את השיטה downloadCallback בתוך פונקציות ה-event 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];
}

אובייקט המטמון הזה מופעל ב-constructor, שנראה עכשיו כך:

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

תחומים שיש לשפר

אין ספק שתצטרכו להשתמש במערכת ניהול נכסים מתקדמת יותר ככל שתתקדמו ביצירת המשחק, אבל אני מקווה שהיא סיפקה לכם התחלה בסיסית. תכונות עתידיות עשויות לכלול:

  • סימון הנכס שבו הייתה שגיאה
  • קריאות חוזרות (callbacks) כדי לציין את ההתקדמות
  • אחזור נכסים מ-File System API

אפשר לפרסם שיפורים, גרסאות משנה וקישורים לקוד בתגובות שבהמשך.

מקור מלא

המקור של מנהל הנכסים הזה והמשחק שממנו הוא מבוסס הוא קוד פתוח במסגרת רישיון Apache, והוא זמין בחשבון GitHub של Bad Aliens. אפשר לשחק במשחק Bad Aliens בדפדפן שתומך ב-HTML5. המשחק הזה היה הנושא של ההרצאה שלי ב-Google IO, שנקראה Super Browser 2 Turbo HD Remix: Introduction to HTML5 Game Development (מצגות, סרטון).

סיכום

ברוב המשחקים יש מנהל נכסים כלשהו, אבל במשחקי HTML5 נדרש מנהל נכסים שיכול לטעון נכסים דרך רשת ולטפל בכשלים. במאמר הזה פירטנו מנהל נכסים פשוט שקל להשתמש בו ולהתאים אותו למשחק ה-HTML5 הבא שלכם. תהנו, ונשמח לשמוע מה דעתכם בתגובות שבהמשך. תודה!