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

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

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

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

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

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

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() ונוסיף פרמטר לקריאה חוזרת (callback):

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

אנחנו נקרא ל-method 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. קבלת אות כשהנכסים מוכנים על ידי הפעלה של פונקציית הקריאה החוזרת (callback)
  5. אחזור נכסים באמצעות getAsset()

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

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

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

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

מקור מלא

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

סיכום

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