מקרה לדוגמה – עכבר קופצני

מבוא

עכבר קופצני

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

העברה של משחק C++ ל-HTML5

עכבר קופצני זמין כרגע ב-Android(C++ ), ב-iOS (C++ ), ב-Windows Phone 7 (C#) וב-Chrome (JavaScript). מדי פעם מוצגת השאלה הבאה: איך כותבים משחק שאפשר לנייד בקלות למספר פלטפורמות? אני מרגישה שאנשים מקווים לקלוע קסם שיוכלו להשתמש בו כדי להגיע לרמת ניידות כזו מבלי להיעזר בניוד. לצערי, לא בטוח שקיים פתרון כזה (המצב הקרוב ביותר הוא מסגרת PlayN של Google או מנוע Unity, אבל אף אחד מהם לא הגיע לכל היעדים שהתעניינתי בהם). הגישה שלי הייתה למעשה חיבור ידני. קודם כתבתי את הגרסה ל-iOS או Android ב-C++ ולאחר מכן העברתי את הקוד הזה לכל פלטפורמה חדשה. זה אולי נשמע כמו עבודה רבה, אבל הגרסה של WP7 ו-Chrome נמשכה לא יותר משבועיים. עכשיו השאלה היא: האם יש משהו שאפשר לעשות כדי ש-codebase יהיה נייד בקלות? היו מספר דברים שעזרו לי לפתור את הבעיה:

שמירה על גודל קטן של Codebase

זה אולי נשמע מובן מאליו, אבל זו הסיבה העיקרית שהצלחתי לנייד את המשחק כל כך מהר. קוד הלקוח של Bouncy Mouse כולל רק כ-7,000 שורות של C++. 7,000 שורות קוד הן לא דבר, אבל הן קטנות מספיק לניהול. בסופו של דבר, שתי גרסאות ה-C# ו-JavaScript של קוד הלקוח היו בערך באותו גודל. שמירת הקוד הבסיסי שלי קטנה בעיקרה בשתי שיטות עבודה עיקריות: אל תכתבו קוד מיותר, ותשתדלו לעשות כמה שיותר בקוד לפני העיבוד (שאינו זמן ריצה). אם לא כותבים קוד מוגזם, אולי ייראה מובן מאליו, אבל זה דבר אחד שאני תמיד נאבקת עם עצמי. לעיתים קרובות יש לי דחף לכתוב שיעור או פונקציה קשורים לכל נושא שאפשר להשקיע בתהליך של כלי עזר. עם זאת, אלא אם אתם מתכננים להשתמש בכלי עזר מספר פעמים, לרוב התהליך נותר ניפוח הקוד. עם העכבר הקופצני, הקפדתי לא לכתוב כלי עזר, אלא אם הייתי משתמש בו לפחות שלוש פעמים. ניסיתי לכתוב קורס עזר, והייתי רוצה לוודא שהוא יהיה נקי, נייד וזמין לשימוש חוזר בפרויקטים שלי בעתיד. מצד שני, כשכתיבת קוד רק עבור עכבר קופצני, עם סבירות נמוכה לשימוש חוזר, התמקדתי בהשלמת משימת התכנות בצורה פשוטה ומהירה ככל האפשר, גם אם זו לא הייתה הדרך "היפה ביותר" לכתוב את הקוד. החלק השני, והחשוב יותר, בשמירה על תשתית קוד קטן, היה לבצע כמה שיותר בדיקות לפני העיבוד. אם אתם יכולים לקחת משימה בסביבת זמן ריצה ולהעביר אותה למשימת עיבוד מראש, לא רק שהמשחק יפעל מהר יותר, אלא גם לא תצטרכו לנייד את הקוד לכל פלטפורמה חדשה. לצורך הדגמה, במקור אחסנתי את נתוני הגיאומטריה של הרמה שלי בפורמט לא מעובד, והרכבתי את מאגרי הקודקוד של OpenGL/WebGL בפועל בזמן הריצה. הפעולה הזו דרשה קצת הגדרה וכמה מאות שורות של קוד זמן ריצה. מאוחר יותר העברתי את הקוד הזה לשלב של עיבוד מקדים, וכתבתי מאגרי קוד מלאים של OpenGL/WebGL קודק בזמן ההידור. כמות הקוד בפועל הייתה כמעט זהה, אבל כמה מאות השורות האלה הועברו לשלב עיבוד מקדים, כך שמעולם לא נאלצתי לנייד אותן לפלטפורמות חדשות. יש המון דוגמאות לכך ב-Bouncy Mouse, מה שאפשר לעשות שונה ממשחק למשחק, אבל חשוב לשים לב לדברים שלא צריכים לקרות בזמן ריצה.

לא כדאי להשתמש בתלות שאינן נחוצות

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

Android iOS HTML5 WP7
גרפיקה OpenGL ES OpenGL ES WebGL XNA
סאונד OpenSL ES OpenAL אודיו באינטרנט XNA
פיזיקה תיבה2D תיבה2D Box2D.js Box2D.xna

זה פחות או יותר. לא נעשה שימוש בספריות גדולות של צד שלישי, מלבד Box2D, ניידת בכל הפלטפורמות. כשמדובר בגרפיקה, גם WebGL וגם XNA מספקים מיפוי כמעט 1:1 באמצעות OpenGL, כך שזו לא הייתה בעיה גדולה. רק בתחום הצלילים הספריות שונות בפועל. עם זאת, קוד הקול ב-Buncy Mouse הוא קוד קטן (כמאה שורות של קוד ספציפי לפלטפורמה), כך שזו לא הייתה בעיה גדולה. כשהעכבר הקופצני נשאר ללא ספריות גדולות ולא ניידות, הלוגיקה של קוד זמן הריצה יכולה להיות כמעט זהה בגרסאות שונות (למרות שינוי השפה). בנוסף, הוא חוסך לנו להינעל בשרשרת כלים לא ניידת. נשאלותי אם תכנות מול OpenGL/WebGL גורם ישירות למורכבות גבוהה יותר בהשוואה לשימוש בספרייה כמו Cocos2D או Unity (יש גם כמה כלים ל-WebGL שאפשר להשתמש בהם). למעשה, אני מאמינה שזה בדיוק ההפך. רוב המשחקים לנייד / משחקי HTML5 (לפחות משחקים כמו Bouncy Mouse) הם פשוטים מאוד. ברוב המקרים, המשחק מצייר רק כמה דמויות Sprite ואולי גם גיאומטריה עם מרקם. המספר הכולל של קוד ספציפי ל-OpenGL בעכבר קופצני הוא כנראה פחות מ-1,000 שורות. אני יהיה מופתע אם שימוש בספריית עזרה באמת יפחית את המספר הזה. גם אם המספר יקטן בחצי, אצטרך להשקיע זמן רב בלמידת ספריות וכלים חדשים רק כדי לחסוך 500 שורות קוד. בנוסף, עדיין לא מצאתי ספרייה משנית שאפשר להשתמש בה בכל הפלטפורמות שמעניינות אותי, כך ששימוש בתלות כזו יפגע משמעותית בניידות. אם הייתי מחבר משחק בתלת-ממד שהיה צריך מפות בהירים, LOD דינמי, אנימציית סקין וכו', התשובה שלי הייתה משתנה ללא ספק. במקרה הזה, הייתי ממציא מחדש את הגלגל כדי לנסות לקודד באופן ידני את כל המנוע שלי מול OpenGL. הנקודה שלי היא שרוב המשחקים לנייד/HTML5 אינם (עדיין) בקטגוריה הזו, לכן אין צורך לסבך דברים לפני שיש צורך.

אל תמעיט בדמיון בין שפות שונות

אחד הטריקים האחרונים שחסך לי הרבה זמן בניוד ה-codebase שלי ב-C++ לשפה חדשה היה להבין שרוב הקוד כמעט זהה בכל שפה. אלמנטים מרכזיים מסוימים עשויים להשתנות, אך מדובר במספר קטן בהרבה של דברים שלא משתנים. למעשה, עבור פונקציות רבות, המעבר מ-C++ ל-JavaScript פשוט היה כרוך בהרצה של כמה החלפות של ביטויים רגולריים ב-codebase שלי ב-C++.

מסקנות מסקנות

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

אודיו

אחד התחומים שגרם לי (ונראה גם לכל השאר) לצרות מסוימות היה האודיו. ב-iOS וב-Android יש כמה אפשרויות אודיו שלמות (OpenSL, OpenAL), אבל בעולם של HTML5, הדברים נראו קודרים יותר. בעוד שאודיו של HTML5 זמין, מצאתי כי יש בו כמה בעיות מפתיעות כאשר משתמשים בו במשחקים. גם בדפדפנים החדשים ביותר, הרבה פעמים נתקלתי בהתנהגות מוזרה. למשל, נראה שיש ל-Chrome מגבלה על מספר רכיבי האודיו שאפשר ליצור בו-זמנית (מקור). בנוסף, גם כשהצליל מושמע, לפעמים לפעמים הסאונד מעוות בצורה לא ברורה. באופן כללי, הייתי קצת מודאג. חיפוש באינטרנט גילה שכמעט לכולם יש בעיה זהה. הפתרון שאליו הגעתי בהתחלה היה API בשם SoundManager2. ממשק API זה משתמש באודיו של HTML5 כאשר הוא זמין, ובמצבים מסובכים הוא משתמש ב-Flash. למרות שהפתרון הזה פעל, הוא עדיין היה מלא באגים ובלתי צפוי (רק פחות מאודיו טהור של HTML5). שבוע אחרי ההשקה, שוחחתי עם כמה מהאנשים המועילים ב-Google שהפנו אותי ל-Web Audio API של Webkit. במקור שקלתי להשתמש ב-API הזה, אבל התרחקתי ממנו בגלל מידת המורכבות המיותרת (עבורי) שיש ל-API. רציתי רק להשמיע כמה צלילים: באודיו של HTML5 מדובר בשתי שורות של JavaScript. עם זאת, במבט קצר על Web Audio נדהמתי מהמפרט העצום (70 דפים), מהכמות הקטנה של דגימות באינטרנט (אופייני לממשק API חדש) והשמטת הפונקציה "play", "השהיה" או "עצירה" במקום כלשהו במפרט. לאחר ש-Google הבטיחה שהחששות שלי לא נוצרו כמו שצריך, התעמקתי שוב ב-API. אחרי עיון בכמה דוגמאות נוספות ומחקר נוסף, גיליתי ש-Google צודקת – ה-API בהחלט יכול לענות על הצרכים שלי, והוא יכול לעשות זאת בלי הבאגים שמדאיגים את ממשקי ה-API האחרים. כדאי במיוחד לקרוא את המאמר תחילת העבודה עם ממשק ה-API של אודיו באינטרנט, שמומלץ להיעזר בו אם רוצים לקבל הבנה עמוקה יותר של ה-API. הבעיה האמיתית שלי היא שאפילו אחרי שהבנתי את ה-API והשתמשתי בו, זה עדיין נראה לי כמו API שלא 'רק להשמיע כמה צלילים'. כדי להתגבר על תחושת הבלגן, כתבתי שיעור עזר קטן שאפשר לי להשתמש ב-API בדיוק כמו שרציתי – להשמיע, להשהות, לעצור, לשלוח שאילתות לגבי מצב הצליל. קראתי לכיתת העזרה הזו AudioClip. המקור המלא זמין ב-GitHub במסגרת רישיון Apache 2.0, ואני אדבר בהמשך על פרטי השיעור. אבל לפני כן, קצת רקע על Web Audio API:

גרפים של אודיו באינטרנט

הדבר הראשון שהופך את Web Audio API למורכב (וחזק יותר) מהרכיב HTML5 Audio הוא היכולת שלו לעבד / לשלב אודיו לפני הפצתו למשתמש. אמנם הפעלת אודיו היא עוצמתית, אבל העובדה שכל הפעלת אודיו כוללת גרף הופכת את העניין למורכב יותר בתרחישים פשוטים. כדי להמחיש את עוצמתו של Web Audio API, ראו את התרשים הבא:

תרשים בסיסי של אודיו באינטרנט
תרשים אודיו בסיסי באינטרנט

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

הגרפים יכולים להיות פשוטים

הדבר הראשון שהופך את Web Audio API למורכב (וחזק יותר) מהרכיב HTML5 Audio הוא היכולת שלו לעבד / לשלב אודיו לפני הפצתו למשתמש. אמנם הפעלת אודיו היא עוצמתית, אבל העובדה שכל הפעלת אודיו כוללת גרף הופכת את העניין למורכב יותר בתרחישים פשוטים. כדי להמחיש את עוצמתו של Web Audio API, ראו את התרשים הבא:

תרשים טריוויאלי של אודיו באינטרנט
Trivial Web Audio Graph

הגרף הטריוויאלי שלמעלה יכול להשיג את כל מה שנדרש להפעלה, השהיה או הפסקה של צליל.

אבל בוא לא לדאוג אפילו לגבי התרשים

למרות שזה נחמד להבין את הגרף, זה לא משהו שאני רוצה להתמודד איתו בכל פעם שאני משמיע צליל. לכן כתבתי כיתת wrapper פשוטה "AudioClip". השיעור הזה מנהל את התרשים הזה באופן פנימי, אבל מציג ממשק API עצמאי הרבה יותר פשוט.

AudioClip
AudioClip

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

// At startup time
var sound = new AudioClip("ping.wav");

// Later
sound.play();

פרטי ההטמעה

בואו נבחן בקצרה את הקוד של כיתת העזרה: Constructor – ה-constructor מטפל בטעינת נתוני הצלילים באמצעות XHR. למרות שהוא לא מוצג כאן (כדי לפשט את הדוגמה), רכיב אודיו של HTML5 יכול לשמש גם כצומת מקור. האפשרות הזו שימושית במיוחד כשמדובר בדגימות גדולות. שימו לב ש-Web Audio API דורש שנאחזר את הנתונים האלה בתור "arraybuffer". אחרי שהנתונים מתקבלים, אנחנו יוצרים מאגר נתונים זמני של 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 היה הביצועים. אחרי שסיימתי את גרסה 1 של היציאה, גיליתי שהכול עובד כמו שצריך במחשב עם ארבע ליבות. לצערי, ב-נטבוק או ב-Chromebook הכול לא היה כל כך טוב. במקרה הזה, ה-profiler של Chrome הציל אותי והראה בדיוק היכן הוקדש כל הזמן שלי לתוכניות. הניסיון שלי מדגיש את החשיבות של יצירת פרופילים לפני שמבצעים אופטימיזציה. ציפיתי שהפיזיקה של Box2D או שקוד הרינדור יהיו מקור עיקרי של האטה. עם זאת, רוב הזמן שלי היה בעבודה מסוימת Matrix.clone(). מכיוון שהמשחק שלי עמוס במתמטיקה, ידעתי שביצעתי הרבה יצירה/שכפול של מטריצות, אבל אף פעם לא ציפיתי שזה יהיה צוואר בקבוק. בסופו של דבר, התברר ששינוי פשוט מאוד מאפשר למשחק לקצר פי 3 את השימוש במעבד (CPU), על ידי מעבר מ-6% ל-7% מהמעבד (CPU) במחשב ל-2%. אולי זה ידע נפוץ למפתחי JavaScript, אבל הבעיה הזו הפתיעה אותי, כי אני מפתחת C++ , אפרט יותר. בעיקרון, המטריצה המקורית שלי הייתה מטריצה של 3x3: מערך של 3 אלמנטים, כל רכיב הכיל מערך של 3 אלמנטים. לצערי, כשרציתי לשכפל את המטריצה, הייתי צריך ליצור 4 מערכים חדשים. השינוי היחיד שהיה עליי לעשות היה להעביר את הנתונים האלה למערך יחיד של 9 רכיבים ולעדכן את החישובים המתמטיים שלי בהתאם. השינוי הזה היה אחראי לחלוטין להפחתת פי 3 את המעבד (CPU) שראיתי, ולאחר השינוי הזה הביצועים היו מקובלים בכל מכשירי הבדיקה שלי.

יותר אופטימיזציה

למרות שהביצועים שלי היו סבירים, עדיין נתקלתי במספר בעיות קלות. אחרי קצת יותר פרופיילינג, הבנתי שהסיבה לכך היא אוסף האשפה של JavaScript. האפליקציה שלי פעלה בקצב של 60fps, כלומר לכל פריים היו רק 16 אלפיות השנייה לצייר. לצערנו, כשאיסוף אשפה מוזן במכונה איטית יותר, לפעמים הוא "אכל" כ-10 אלפיות שנייה. התוצאה גרמה לתזוזה תוך כמה שניות, כיוון שהמשחק דרש כמעט 16 אלפיות השנייה במלואם כדי לצייר פריים מלא. כדי להבין טוב יותר למה ייצרתי כל כך הרבה אשפה, השתמשתי בכלי ליצירת תמונת מצב של ערימה ב-Chrome. לצערי, התברר שהרוב המכריע (יותר מ-70%) של האשפה נוצר על ידי Box2D. סילוק אשפה ב-JavaScript הוא עסק מסובך, ושכתיבה מחדש של Box2D לא הייתה נכונה, אז הבנתי שהצלחתי להיכנס לפינה. למזלי, אחד מהטריקים הישנים ביותר בספר שלי עדיין זמין: כשאי אפשר להגיע ל-60fps, מומלץ לרוץ במהירות של 30fps. אנחנו די מסכימים שריצה בקצב קבוע של 30fps עדיפה הרבה יותר מריצה בקצב של 60fps. למעשה עדיין לא קיבלתי תלונה אחת או הערה אחת על כך שהמשחק פועל בקצב של 30fps (קשה מאוד להבחין אם לא משווים בין שתי הגרסאות זו לצד זו). תוספת הזמן של 16 אלפיות השנייה לכל פריים הייתה שאפילו במקרה של אוסף אשפה מכוער, עדיין היה לי הרבה זמן לעבד את הפריים. הפעלה של קצב של 30fps לא מופעלת באופן מפורש על ידי ה-API לתזמון שבו השתמשתי (requestAnimationFrame של WebKit, אבל ניתן לבצע אותה באופן טריוויאלי מאוד. למרות שהיא אולי לא אלגנטית כמו ממשק API מפורש, ניתן להשיג 30fps על ידי ידיעה שמרווח ה-RequestAnimationFrame מותאם ל-VSYNC (בדרך כלל 60fps) של המסך. המשמעות היא שאנחנו צריכים פשוט להתעלם מכל קריאה חוזרת אחרת. בעיקרון, אם יש לכם קריאה חוזרת (Tick) שנקראת בכל פעם ש-RequestAnimationFrame מופעלת, ניתן לבצע זאת באופן הבא:

var skip = false;

function Tick() {
skip = !skip;
if (skip) {
return;
}

// OTHER CODE
}

אם ברצונך להיזהר במיוחד, עליך לבדוק שה-VSYNC של המחשב עדיין לא פועל במהירות של 30fps או פחות בזמן ההפעלה, ולהשבית את הדילוג במקרה זה. עם זאת, עדיין לא ראיתי את זה באף הגדרה של מחשב שולחני או נייד שבדקתי.

הפצה ומונטיזציה

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

Bouncy Mouse הושק בסוף אוקטובר בחנות האינטרנט של Chrome. פרסום בחנות האינטרנט של Chrome מאפשר לי להשתמש במערכת קיימת כדי להגביר את יכולת הגילוי, לשפר את המעורבות בקהילה, את הדירוגים ותכונות אחרות שהתרגשתי אליהן בפלטפורמות לנייד. מה שהפתיע אותי היה היקף החשיפה של החנות. תוך חודש מתאריך הפרסום הגעתי לכמעט ארבע מאות אלף התקנות, וכבר נהניתי מהמעורבות עם הקהילה (דיווח על באגים, משוב). דבר נוסף שהפתיע אותי היה הפוטנציאל של אפליקציית אינטרנט לייצר הכנסות.

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

רווחים מנורמלים לאורך זמן.
רווחים מנורמלים לאורך זמן

למרות שהרווחים מהמשחק היו טובים בהרבה מהצפוי, כדאי לציין שטווח ההגעה של חנות האינטרנט של Chrome עדיין קטן יותר מזה של פלטפורמות בוגרות יותר כמו Android Market. אמנם העכבר הקופצני הצליח להגיע במהירות למשחק מס' 9 בחנות האינטרנט של Chrome, אבל שיעור המשתמשים החדשים שמגיעים לאתר האט באופן משמעותי מאז ההשקה הראשונית. עם זאת, המשחק עדיין רואה צמיחה יציבה, ואני שמח לראות מה יהיו התפתחותו של הפלטפורמה!

סיכום

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