מבוא
אחרי שפרסמתי את Bouncy Mouse ל-iOS ול-Android בסוף השנה שעברה, למדתי כמה לקחים חשובים מאוד. אחד מהם היה שקשה לפרוץ לשוק קיים. בשוק המוצף של מכשירי iPhone, היה קשה מאוד לקבל תנועה. בשוק Android Marketplace, פחות מוצף, היה קל יותר להתקדם, אבל עדיין לא קל. בעקבות החוויה הזו, ראיתי הזדמנות מעניינת בחנות האינטרנט של Chrome. חנות האינטרנט לא ריקה, אבל הקטלוג שלה של משחקים איכותיים מבוססי HTML5 רק מתחיל להתפתח. מפתחי אפליקציות חדשים יכולים להשתמש בנתונים האלה כדי להבין מהם הגורמים שמשפיעים על דירוג האפליקציה שלהם, וכך להגדיל את החשיפה שלה. בהתאם להזדמנות הזו, התחלתי להעביר את Bouncy Mouse ל-HTML5, בתקווה שאוכל לספק את חוויית המשחק העדכנית ביותר שלי לבסיס משתמשים חדש ומרגש. בניתוח המקרה הזה, אדבר קצת על התהליך הכללי של העברת Bouncy Mouse ל-HTML5, ואז אעמיק קצת יותר בשלושה תחומים שהיו מעניינים: אודיו, ביצועים ומונטיזציה.
העברה של משחק ב-C++ ל-HTML5
התכונה Bouncy Mouse זמינה כרגע ב-Android(C++), ב-iOS (C++), ב-Windows Phone 7 (C#) וב-Chrome (Javascript). לפעמים עולה השאלה: איך כותבים משחק שאפשר להעביר בקלות לפלטפורמות מרובות? נראה לי שאנשים מחפשים פתרון קסם שיעזור להם להגיע לרמת הניידות הזו בלי להשתמש בהעברה ידנית. לצערי, לא בטוח שפתרון כזה קיים עדיין (הפתרון הקרוב ביותר הוא כנראה מסגרת PlayN של Google או מנוע Unity, אבל אף אחד מהם לא עומד בכל היעדים שהתעניינתי בהם). הגישה שלי הייתה, למעשה, ניוד ידני. קודם כתבתי את הגרסה ל-iOS ול-Android ב-C++, ואז העברתי את הקוד הזה לכל פלטפורמה חדשה. אולי זה נשמע כמו הרבה עבודה, אבל השלמת הגרסאות ל-WP7 ול-Chrome ארכה לא יותר משבועיים. אז השאלה היא, האם אפשר לעשות משהו כדי שקוד בסיס יהיה נייד בקלות? עשיתי כמה דברים שיעזרו לך:
שמירה על בסיס הקוד קטן
אולי זה נראה ברור, אבל זו באמת הסיבה העיקרית לכך שהצלחתי להעביר את המשחק במהירות כה רבה. קוד הלקוח של Bouncy Mouse מכיל רק כ-7,000 שורות ב-C++. 7,000 שורות קוד הן לא מעט, אבל הן קטנות מספיק כדי שניתן יהיה לנהל אותן. בסופו של דבר, שתי הגרסאות של קוד הלקוח – C# ו-JavaScript – היו באותו גודל בערך. כדי לשמור על קוד בסיס קטן, השתמשתי בשתי שיטות עיקריות: לא כתבתי קוד מיותר, וביצעתי כמה שיותר קוד בתהליך העיבוד המקדים (לא בזמן הריצה). אולי נראה ברור שאין לכתוב קוד מיותר, אבל זהו אחד הדברים שאני תמיד נלחם בעצמי עליהם. לעתים קרובות יש לי צורך לכתוב פונקציה או סוג עזר לכל דבר שאפשר להפוך לעזר. עם זאת, אם אתם לא מתכננים להשתמש בפונקציית העזרה כמה פעמים, בדרך כלל היא רק תגרום לקוד להיות מיותר. ב-Bouncy Mouse, הקפדתי לא לכתוב פונקציית עזר אלא אם הייתי מתכוון להשתמש בה לפחות שלוש פעמים. כשכתבתי את הכיתה לעזרה, ניסיתי ליצור אותה נקייה, ניידת וניתן לשימוש חוזר בפרויקטים עתידיים. לעומת זאת, כשכותבים קוד רק ל-Bouncy Mouse, עם סיכוי נמוך לשימוש חוזר, ההתמקדות היא בהשלמת משימת הקוד בצורה פשוטה ומהירה ככל האפשר, גם אם זו לא הדרך 'הכי יפה' לכתוב את הקוד. החלק השני, והחשוב יותר, לשמירה על קוד בסיס קטן היה להעביר כמה שיותר שלבים לעיבוד מקדים. אם תוכלו להעביר משימה בסביבת זמן הריצה למשימה של עיבוד מקדים, המשחק יפעל מהר יותר, וגם לא תצטרכו להעביר את הקוד לכל פלטפורמה חדשה. לדוגמה, בהתחלה שמרתי את נתוני הגיאומטריה של הרמה בפורמט לא מעובד למדי, והרכבתי את מאגרי הנקודות בפועל של OpenGL/WebGL בזמן הריצה. לשם כך נדרשו כמה שורות קוד בסביבת זמן הריצה וכמה שעות הגדרה. מאוחר יותר העברתי את הקוד הזה לשלב עיבוד מקדים, והוספת כתיבה של מאגרי קודקודים של OpenGL/WebGL בזמן הידור. כמות הקוד בפועל הייתה בערך זהה, אבל כמה מאות השורות האלה הועברו לשלב עיבוד מקדים, כך שמעולם לא הייתי צריך להעביר אותן לפלטפורמות חדשות. יש המון דוגמאות לכך במשחק Bouncy Mouse, והאפשרויות משתנות ממשחק למשחק, אבל חשוב לשים לב לכל דבר שלא צריך לקרות בזמן הריצה.
לא להשתמש ביחסי תלות שלא נחוצים
סיבה נוספת לכך שקל להעביר את Bouncy Mouse היא שאין לה כמעט יחסי תלות. התרשים הבא מסכם את יחסי התלות העיקריים של Bouncy Mouse בספריות לפי פלטפורמה:
זה בערך הכול. לא נעשה שימוש בספריות גדולות של צד שלישי, מלבד Box2D, שניתנת להעברה לכל הפלטפורמות. לגבי גרפיקה, גם WebGL וגם XNA ממפים כמעט ביחס 1:1 ל-OpenGL, כך שלא הייתה בעיה גדולה. רק בתחום האודיו הספריות בפועל היו שונות. עם זאת, קוד האודיו ב-Bouncy Mouse הוא קטן (כמאה שורות של קוד ספציפי לפלטפורמה), כך שלא הייתה זו בעיה גדולה. העובדה ש-Bouncy Mouse לא מכילה ספריות גדולות שלא ניתן להעביר פירושה שהלוגיקה של קוד זמן הריצה יכולה להיות כמעט זהה בין הגרסאות (למרות שינוי השפה). בנוסף, הוא מונע מאיתנו להיתקע בשרשרת כלים לא ניידת. שאלתי אם תכנות ישירות ב-OpenGL/WebGL גורמת לעלייה ברמת המורכבות בהשוואה לשימוש בספרייה כמו Cocos2D או Unity (יש גם כמה ספריות עזר ל-WebGL). למעשה, אני מאמין בדיוק בהפך. רוב המשחקים לנייד או למכשירים עם HTML5 (לפחות משחקים כמו Bouncy Mouse) הם פשוטים מאוד. ברוב המקרים, המשחק רק מצייר כמה ספרייטים ואולי קצת גיאומטריה עם טקסטורה. סביר להניח שסך כל הקוד הספציפי ל-OpenGL ב-Bouncy Mouse הוא פחות מ-1,000 שורות. אני לא חושב ששימוש בספריית עזר יביא להפחתה של המספר הזה. גם אם המספר הזה יתכווץ לחצי, אצטרך להשקיע זמן רב בלמידת ספריות או כלים חדשים רק כדי לחסוך 500 שורות קוד. בנוסף, עדיין לא מצאתי ספריית עזר שניתן להעביר לכל הפלטפורמות שמעניינות אותי, כך ששימוש בספרייה כזו יגרום לפגיעה משמעותית ביכולת ההעברה. אם הייתי כותב משחק תלת-ממדי שצריך מפות אור, רמת פירוט דינמית, אנימציה עם עור וכו', התשובה שלי הייתה משתנה בוודאות. במקרה כזה, אצטרך להמציא מחדש את הגלגל כדי לנסות לכתוב ביד את כל המנוע שלי מול OpenGL. הנקודה שלי היא שרוב המשחקים לנייד או ב-HTML5 לא נכללים (עדיין) בקטגוריה הזו, ולכן אין צורך לסבך את העניינים לפני שזה הכרחי.
אל תזלזלו בדמיון בין שפות
טיפ אחרון שחסך לי הרבה זמן בהעברת קוד ה-C++ שלי לשפה חדשה היה ההבנה שרוב הקוד כמעט זהה בכל שפה. יכול להיות שחלק מהרכיבים המרכזיים ישתנו, אבל הם פחותים בהרבה מאלה שלא משתנים. למעשה, בפונקציות רבות, המעבר מ-C++ ל-JavaScript כלל רק הרצת כמה החלפות של ביטויים רגולריים בקוד הבסיסי שלי ב-C++.
מסקנות לגבי ההעברה
זהו בערך כל תהליך הניוד. בקטעים הבאים אגע בכמה אתגרים ספציפיים ל-HTML5, אבל המסר העיקרי הוא שאם תשתמשו בקוד פשוט, ההעברה תהיה כאב ראש קטן ולא סיוט.
אודיו
אחת מהבעיות שהיו לי (ולכאורה לכל השאר) הייתה איכות האודיו. ב-iOS וב-Android יש כמה אפשרויות אודיו טובות (OpenSL, OpenAL), אבל בעולם של HTML5 המצב היה פחות מבטיח. אודיו ב-HTML5 זמין, אבל גיליתי שיש לו כמה בעיות קריטיות כשמשתמשים בו במשחקים. גם בדפדפנים החדשים ביותר, נתקלתי לעיתים קרובות בהתנהגות מוזרה. לדוגמה, נראה שיש ב-Chrome מגבלה על מספר רכיבי האודיו (source) שאפשר ליצור בו בו-זמנית. בנוסף, גם כשהצליל הופעל, לפעמים הוא היה מתעוות באופן לא מוסבר. באופן כללי, הייתי קצת מודאג. חיפוש באינטרנט העלה שזו בעיה שקיימת כמעט אצל כולם. הפתרון הראשון שמצאתי היה ממשק API שנקרא SoundManager2. כשהאפשרות זמינה, ממשק ה-API הזה משתמש ב-HTML5 Audio, ובמצבים בעייתיים הוא עובר ל-Flash. הפתרון הזה עבד, אבל עדיין היו בו באגים והוא לא היה צפוי (רק פחות מאשר HTML5 Audio טהור). שבוע אחרי ההשקה, דיברתי עם כמה אנשים מועילים ב-Google, שהפנו אותי ל-Web Audio API של Webkit. בהתחלה חשבתי להשתמש ב-API הזה, אבל ויתרתי עליו בגלל רמת המורכבות המיותרת (לדעתי) של ה-API. רציתי רק להשמיע כמה צלילים: עם HTML5 Audio, זה כרוך בכמה שורות של JavaScript. עם זאת, במבט הקצר שלי על Web Audio, הופתעתי מהמפרט הענק שלו (70 דפים), מכמות הדוגמאות הקטנה באינטרנט (אופיינית לממשק API חדש) ומכך שלא נכללה בו פונקציית 'הפעלה', 'השהיה' או 'עצירה' בשום מקום במפרט. אחרי שקיבלתי מ-Google ביטחון שהחששות שלי לא מבוססים, חפרתי שוב ב-API. אחרי שבדקתי עוד כמה דוגמאות ועשיתי קצת מחקר נוסף, הבנתי ש-Google צדקה – ה-API בהחלט יכול לענות על הצרכים שלי, והוא יכול לעשות זאת בלי הבאגים שמאפיינים את ממשקי ה-API האחרים. מאמר שימושי במיוחד הוא תחילת העבודה עם Web Audio API, שמספק הסבר מעמיק על ה-API. הבעיה האמיתית שלי היא שגם אחרי שהבנתי את ה-API והשתמשתי בו, עדיין נראה לי שמדובר ב-API שלא מיועד "רק להפעלת כמה צלילים". כדי לעקוף את הבעיה הזו, כתבתי מחלקת עזרה קטנה שאפשר להשתמש בה ב-API בדיוק כמו שרציתי – להפעיל, להשהות, לעצור ולבצע שאילתות לגבי המצב של צליל. קראתי לכיתה העזרה הזו AudioClip. המקור המלא זמין ב-GitHub בכפוף לרישיון Apache 2.0, ואתאר בהמשך את פרטי הכיתה. אבל קודם, קצת רקע על Web Audio API:
תרשימים של Web Audio
הדבר הראשון שגורם ל-Web Audio API להיות מורכב יותר (וחזק יותר) מרכיב האודיו של HTML5 הוא היכולת שלו לעבד או לערבב אודיו לפני שהוא מועבר כפלט למשתמש. העובדה שכל הפעלת אודיו כוללת תרשים היא תכונה חזקה, אבל היא גם הופכת את התרחישים הפשוטים למורכבים יותר. כדי להמחיש את העוצמה של Web Audio API, כדאי לעיין בתרשים הבא:
הדוגמה שלמעלה ממחישה את העוצמה של Web Audio API, אבל לא הייתי צריך את רוב העוצמה הזו בתרחיש שלי. רציתי רק להשמיע צליל. עדיין נדרשת כאן שימוש בגרף, אבל הגרף הזה פשוט מאוד.
תרשימים יכולים להיות פשוטים
הדבר הראשון שגורם ל-Web Audio API להיות מורכב יותר (וחזק יותר) מרכיב האודיו של HTML5 הוא היכולת שלו לעבד או לערבב אודיו לפני שהוא מועבר כפלט למשתמש. העובדה שכל הפעלת אודיו כוללת תרשים היא תכונה חזקה, אבל היא גם הופכת את התרחישים הפשוטים למורכבים יותר. כדי להמחיש את העוצמה של Web Audio API, כדאי לעיין בתרשים הבא:
התרשים הפשוט שמוצג למעלה יכול לבצע את כל הפעולות הנדרשות להפעלה, להשהיה או להפסקה של צליל.
אבל לא צריך לדאוג בכלל לתרשים
קל להבין את התרשים, אבל לא רוצה להתעסק בו בכל פעם שמפעילים צליל. לכן, כתבתי את מחלקת האריזה הפשוטה 'AudioClip'. המחלקה הזו מנהלת את התרשים הזה באופן פנימי, אבל מציגה ממשק API הרבה יותר פשוט למשתמש.
הכיתה הזו היא לא יותר מאשר תרשים של Web Audio ומצבי עזרה מסוימים, אבל היא מאפשרת לי להשתמש בקוד פשוט הרבה יותר מאשר אם הייתי צריך ליצור תרשים של Web Audio כדי להשמיע כל צליל.
// At startup time
var sound = new AudioClip("ping.wav");
// Later
sound.play();
פרטי ההטמעה
נעיף מבט מהיר בקוד של הכיתה לעזרה: Constructor – ה-constructor מטפל בחיוב של נתוני האודיו באמצעות XHR. לא מוצג כאן (כדי שהדוגמה תהיה פשוטה), אבל אפשר להשתמש גם ברכיב אודיו של HTML5 כצומת מקור. האפשרות הזו שימושית במיוחד לדגימות גדולות. חשוב לזכור ש-Web Audio API מחייב אותנו לאחזר את הנתונים האלה כ-"arraybuffer". אחרי שהנתונים מתקבלים, אנחנו יוצרים מהם מאגר (buffer) של Web Audio (מקודדים אותם מהפורמט המקורי לפורמט PCM בסביבת זמן הריצה).
/**
* Create a new AudioClip object from a source URL. This object can be played,
* paused, stopped, and resumed, like the HTML5 Audio element.
*
* @constructor
* @param {DOMString} src
* @param {boolean=} opt_autoplay
* @param {boolean=} opt_loop
*/
AudioClip = function(src, opt_autoplay, opt_loop) {
// At construction time, the AudioClip is not playing (stopped),
// and has no offset recorded.
this.playing_ = false;
this.startTime_ = 0;
this.loop_ = opt_loop ? true : false;
// State to handle pause/resume, and some of the intricacies of looping.
this.resetTimout_ = null;
this.pauseTime_ = 0;
// Create an XHR to load the audio data.
var request = new XMLHttpRequest();
request.open("GET", src, true);
request.responseType = "arraybuffer";
var sfx = this;
request.onload = function() {
// When audio data is ready, we create a WebAudio buffer from the data.
// Using decodeAudioData allows for async audio loading, which is useful
// when loading longer audio tracks (music).
AudioClip.context.decodeAudioData(request.response, function(buffer) {
sfx.buffer_ = buffer;
if (opt_autoplay) {
sfx.play();
}
});
}
request.send();
}
הפעלה – הפעלת הצליל שלנו כוללת שני שלבים: הגדרת תרשים ההפעלה והפעלת גרסה של 'noteOn' במקור של התרשים. אפשר להפעיל מקור רק פעם אחת, לכן אנחנו צריכים ליצור מחדש את המקור או את התרשים בכל פעם שאנחנו מפעילים אותם.
רוב המורכבות של הפונקציה הזו נובעת מהדרישות הנדרשות כדי להמשיך קליפ מושהה (this.pauseTime_ > 0
). כדי להמשיך את ההפעלה של קליפ מושהה, אנחנו משתמשים ב-noteGrainOn
, שמאפשר להפעיל תת-אזור של מאגר. לצערנו, noteGrainOn
לא יוצר אינטראקציה עם הלולאה בדרך הרצויה בתרחיש הזה (הוא יבצע לולאה באזור המשנה, ולא במאגר כולו).
לכן, אנחנו צריכים לעקוף את הבעיה הזו על ידי הפעלת שאר הקליפ באמצעות noteGrainOn
, ואז הפעלה מחדש של הקליפ מההתחלה עם הפעלה חוזרת.
/**
* Recreates the audio graph. Each source can only be played once, so
* we must recreate the source each time we want to play.
* @return {BufferSource}
* @param {boolean=} loop
*/
AudioClip.prototype.createGraph = function(loop) {
var source = AudioClip.context.createBufferSource();
source.buffer = this.buffer_;
source.connect(AudioClip.context.destination);
// Looping is handled by the Web Audio API.
source.loop = loop;
return source;
}
/**
* Plays the given AudioClip. Clips played in this manner can be stopped
* or paused/resumed.
*/
AudioClip.prototype.play = function() {
if (this.buffer_ && !this.isPlaying()) {
// Record the start time so we know how long we've been playing.
this.startTime_ = AudioClip.context.currentTime;
this.playing_ = true;
this.resetTimeout_ = null;
// If the clip is paused, we need to resume it.
if (this.pauseTime_ > 0) {
// We are resuming a clip, so it's current playback time is not correctly
// indicated by startTime_. Correct this by subtracting pauseTime_.
this.startTime_ -= this.pauseTime_;
var remainingTime = this.buffer_.duration - this.pauseTime_;
if (this.loop_) {
// If the clip is paused and looping, we need to resume the clip
// with looping disabled. Once the clip has finished, we will re-start
// the clip from the beginning with looping enabled
this.source_ = this.createGraph(false);
this.source_.noteGrainOn(0, this.pauseTime_, remainingTime)
// Handle restarting the playback once the resumed clip has completed.
// *Note that setTimeout is not the ideal method to use here. A better
// option would be to handle timing in a more predictable manner,
// such as tying the update to the game loop.
var clip = this;
this.resetTimeout_ = setTimeout(function() { clip.stop(); clip.play() },
remainingTime * 1000);
} else {
// Paused non-looping case, just create the graph and play the sub-
// region using noteGrainOn.
this.source_ = this.createGraph(this.loop_);
this.source_.noteGrainOn(0, this.pauseTime_, remainingTime);
}
this.pauseTime_ = 0;
} else {
// Normal case, just creat the graph and play.
this.source_ = this.createGraph(this.loop_);
this.source_.noteOn(0);
}
}
}
הפעלה כקובץ אודיו – פונקציית ההפעלה שלמעלה לא מאפשרת להפעיל את קטע האודיו כמה פעמים עם חפיפה (הפעלה שנייה אפשרית רק כשהקליפ מסתיים או מושהה). לפעמים במשחקים רוצים להשמיע צליל פעמים רבות בלי להמתין להשלמת כל הפעלה (איסוף מטבעות במשחק וכו'). כדי לעשות זאת, לכיתה AudioClip יש את השיטה playAsSFX()
.
מאחר שיכולות להתרחש כמה הפעלות בו-זמנית, ההפעלה מ-playAsSFX()
לא קשורה ביחס 1:1 ל-AudioClip. לכן אי אפשר להפסיק, להשהות או לשלוח שאילתה לגבי המצב של ההפעלה. גם האפשרות להפעלה בלופ מושבתת, כי לא תהיה דרך להפסיק צליל שמושמע בלופ באופן הזה.
/**
* Plays the given AudioClip as a sound effect. Sound Effects cannot be stopped
* or paused/resumed, but can be played multiple times with overlap.
* Additionally, sound effects cannot be looped, as there is no way to stop
* them. This method of playback is best suited to very short, one-off sounds.
*/
AudioClip.prototype.playAsSFX = function() {
if (this.buffer_) {
var source = this.createGraph(false);
source.noteOn(0);
}
}
עצירה, השהיה וסטטוס של שליחת שאילתות – שאר הפונקציות הן די ישרות ולא דורשות הרבה הסברים:
/**
* Stops an AudioClip , resetting its seek position to 0.
*/
AudioClip.prototype.stop = function() {
if (this.playing_) {
this.source_.noteOff(0);
this.playing_ = false;
this.startTime_ = 0;
this.pauseTime_ = 0;
if (this.resetTimeout_ != null) {
clearTimeout(this.resetTimeout_);
}
}
}
/**
* Pauses an AudioClip. The offset into the stream is recorded to allow the
* clip to be resumed later.
*/
AudioClip.prototype.pause = function() {
if (this.playing_) {
this.source_.noteOff(0);
this.playing_ = false;
this.pauseTime_ = AudioClip.context.currentTime - this.startTime_;
this.pauseTime_ = this.pauseTime_ % this.buffer_.duration;
this.startTime_ = 0;
if (this.resetTimeout_ != null) {
clearTimeout(this.resetTimeout_);
}
}
}
/**
* Indicates whether the sound is playing.
* @return {boolean}
*/
AudioClip.prototype.isPlaying = function() {
var playTime = this.pauseTime_ +
(AudioClip.context.currentTime - this.startTime_);
return this.playing_ && (this.loop_ || (playTime < this.buffer_.duration));
}
סיכום האודיו
אני מקווה ששיעור העזרה הזה יעזור למפתחים שנתקלים באותן בעיות אודיו כמוני. בנוסף, שיעור כזה נראה מקום סביר להתחיל בו גם אם אתם צריכים להוסיף כמה מהתכונות החזקות יותר של Web Audio API. כך או כך, הפתרון הזה ענה על הצרכים של Bouncy Mouse, והמשחק הפך למשחק HTML5 אמיתי, ללא תנאים מוקדמים.
ביצועים
תחום נוסף שדאגתי לגביו לגבי יצירת יציאה ל-JavaScript היה הביצועים. אחרי שסיימתי את הגרסה הראשונה של ההעברה, גיליתי שהכול פועל כמו שצריך במחשב האישי עם ארבעה ליבות. לצערנו, המצב היה קצת פחות טוב במחשבים ניידים או ב-Chromebook. במקרה הזה, הכלי לניתוח פרופיל של Chrome הציל אותי על ידי הצגת המקום המדויק שבו כל הזמן של התוכניות שלי הלך.
מניסיוני, חשוב ליצור פרופיל לפני שמבצעים אופטימיזציה. ציפיתי שהפיזיקה של Box2D או אולי קוד העיבוד יהיו המקור העיקרי להאטה, אבל רוב הזמן שלי הושקע בפונקציה Matrix.clone()
. מכיוון שהמשחק שלי מבוסס על מתמטיקה, ידעתי שיצרתי או העתקתי הרבה מטריצות, אבל אף פעם לא ציפיתי שזו תהיה נקודת הצוואר בקו. בסופו של דבר, התברר ששינוי פשוט מאוד אפשר למשחק לצמצם את השימוש שלו במעבד ביותר מפי 3, מ-6-7% מעבד במחשב שלי ל-2%.
יכול להיות שזה ידוע למפתחי JavaScript, אבל כמפתח C++ הבעיה הזו הפתיעה אותי, ולכן ארחיב עליה קצת. בעיקרון, מחלקת המטריצה המקורית שלי הייתה מטריצה 3x3: מערך של 3 רכיבים, כל רכיב מכיל מערך של 3 רכיבים. לצערנו, המשמעות היא שכשהגעתי לשלב של יצירת העותקים המרובים של המטריצה, נאלצתי ליצור 4 מערכי משנה חדשים. השינוי היחיד שצריך לבצע הוא להעביר את הנתונים האלה למערך יחיד של 9 רכיבים ולעדכן את החישובים בהתאם. השינוי הזה היה אחראי לחלוטין לירידה של פי 3 בשימוש ב-CPU שראיתי, ואחרי השינוי הזה הביצועים היו בטווח הקביל בכל מכשירי הבדיקה שלי.
אופטימיזציה נוספת
הביצועים שלי היו סבירים, אבל עדיין היו כמה בעיות קלות. אחרי קצת יותר פרופילים, הבנתי שזה בגלל אוסף ה-Garbage של Javascript. האפליקציה שלי רצה בקצב של 60fps, כלומר לכל פריים היו רק 16 אלפיות השנייה לציור. לצערנו, כשאוספים גרוטאות במכונה איטית יותר, לפעמים הזמן הזה נמשך כ-10 אלפיות השנייה. כתוצאה מכך, המשחק התנהל בתנודות מדי כמה שניות, כי נדרשו לו כמעט כל 16 אלפיות השנייה כדי לצייר פריים מלא. כדי להבין טוב יותר למה נוצר כל כך הרבה '', השתמשתי ב-heap profiler של Chrome. לצערי הרב, התברר שרוב האשפה (יותר מ-70%) נוצרה על ידי Box2D. קשה מאוד להסיר נתונים מיותרים ב-Javascript, וכתיבה מחדש של Box2D לא הייתה אופציה, אז הבנתי שנכנסתי לבעיה. למזלי, עדיין היה לי אחד מהטריקים הישנים ביותר שאפשר להשתמש בהם: אם אי אפשר להגיע ל-60fps, מריצים ב-30fps. רוב האנשים מסכימים שעדיף להריץ את הסרטון בקצב קבוע של 30fps מאשר בקצב קפיצי של 60fps. למעשה, עדיין לא קיבלתי תלונה או תגובה אחת על כך שהמשחק פועל במהירות 30fps (קשה מאוד לדעת אם לא משווים בין שתי הגרסאות זו לצד זו). 16 אלפיות השנייה הנוספות לכל פריים המשמעות שלהן היא שגם במקרה של איסוף גרוטאות מכוער, עדיין היה לי מספיק זמן לעבד את הפריים. ה-timing API שבו השתמשתי (requestAnimationFrame המעולה של WebKit) לא מאפשר להפעיל את התצוגה במהירות של 30fps באופן מפורש, אבל אפשר לעשות זאת בקלות רבה. אמנם הפתרון הזה לא אלגנטי כמו API מפורש, אבל אפשר להשיג 30fps אם יודעים שהמרווח של RequestAnimationFrame תואם ל-VSYNC של המסך (בדרך כלל 60fps). כלומר, אנחנו פשוט צריכים להתעלם מכל קריאה חוזרת אחרת. בעיקרון, אם יש לכם פונקציית קריאה חוזרת (callback) בשם 'Tick' שנקראת בכל פעם ש-'RequestAnimationFrame' מופעלת, אפשר לעשות זאת באופן הבא:
var skip = false;
function Tick() {
skip = !skip;
if (skip) {
return;
}
// OTHER CODE
}
אם אתם רוצים להיות זהירים במיוחד, כדאי לבדוק שה-VSYNC של המחשב לא נמצא כבר ב-30fps או מתחת ל-30fps בזמן ההפעלה, ולהשבית את הדילוג במקרה כזה. עם זאת, עדיין לא ראיתי את זה בהגדרות של מחשבים נייחים או ניידים שבדקתי.
הפצה ומונטיזציה
אזור אחר שהפתיע אותי בגרסה של Bouncy Mouse ל-Chrome הוא המונטיזציה. כשהתחלתי את הפרויקט, ראיתי במשחקי HTML5 ניסוי מעניין שיעזור לי ללמוד על טכנולוגיות מתפתחות. לא הבנתי שהגרסה הזו תגיע לקהל גדול מאוד ותהיה לה פוטנציאל משמעותי לייצור הכנסות.
המשחק Bouncy Mouse הושק בסוף אוקטובר בחנות האינטרנט של Chrome. כשפרסמתי את התוסף בחנות האינטרנט של Chrome, יכולתי להשתמש במערכת קיימת לשיפור החשיפה, ליצירת מעורבות בקרב הקהילה, לדירוגים ולתכונות אחרות שהתרגלתי אליהן בפלטפורמות לנייד. הפתיע אותי עד כמה החנות הגיעה לקהל רחב. תוך חודש מהשקה, הגעתי לכמעט ארבע מאות אלף התקנות וכבר נהניתי מהמעורבות של הקהילה (דיווח על באגים, משוב). דבר נוסף שהפתיע אותי היה הפוטנציאל לייצור הכנסות מאפליקציית אינטרנט.
במשחק Bouncy Mouse יש שיטה פשוטה אחת למונטיזציה – מודעת באנר לצד תוכן המשחק. עם זאת, לאור פוטנציאל החשיפה הרחב של המשחק, גיליתי שמודעת הבאנר הזו הצליחה לייצר הכנסה משמעותית, ובמהלך תקופת השיא שלה, האפליקציה הניבה הכנסה שדומה להכנסה בפלטפורמה הכי מוצלחת שלי, Android. אחד הגורמים לכך הוא שמודעות AdSense הגדולות יותר שמוצגות בגרסה של HTML5 מניבות הכנסה גבוהה יותר משמעותית לכל חשיפה בהשוואה למודעות AdMob הקטנות יותר שמוצגות ב-Android. בנוסף, מודעת הבאנר בגרסה ל-HTML5 פחות פולשנית בהשוואה לגרסה ל-Android, וכך חוויית המשחק נקייה יותר. באופן כללי, הופתעתי לטובה מהתוצאה הזו.

הרווחים מהמשחק היו טובים בהרבה מהצפוי, אבל חשוב לציין שהיקף החשיפה של חנות האינטרנט של Chrome עדיין קטן יותר מזה של פלטפורמות מפותחות יותר כמו Android Market. המשחק Bouncy Mouse הצליח להגיע במהירות למקום ה-9 ברשימת המשחקים הפופולריים ביותר בחנות האינטרנט של Chrome, אבל קצב הצירוף של משתמשים חדשים לאתר השתנה באופן משמעותי מאז ההשקה הראשונית. עם זאת, המשחק עדיין נמצא בצמיחה מתמדת ואני שמח לראות לאן הפלטפורמה תתפתח.
סיכום
העברת Bouncy Mouse ל-Chrome הייתה חלקה הרבה יותר ממה שציפיתי. מלבד כמה בעיות קלות באודיו ובביצועים, גיליתי ש-Chrome היא פלטפורמה מתאימה בהחלט למשחק קיים לנייד. אני ממליץ למפתחים שלא ניסו את התכונה הזו לנסות אותה. הייתי מרוצה מאוד מתהליך ההעברה ומהקהל החדש של גיימרים שהמשחק ב-HTML5 קישר אותי אליו. אם יש לך שאלות, אפשר לשלוח לי אימייל. אפשר גם להשאיר תגובה למטה, ואנסה לבדוק את התגובות האלה באופן קבוע.