מקרה לדוגמה – בתוך המבוך ברחבי העולם

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

מבוך עולמי

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

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

DeviceOrientation

אירוע DeviceOrientation (דוגמה) משמש לאחזור נתוני ההטיה מהסמארטפון. בשימוש ב-addEventListener עם האירוע DeviceOrientation, קריאה חוזרת (callback) עם האובייקט DeviceOrientationEvent מופעלת כארגומנט במרווחי זמן קבועים. מרווחי הזמן עצמם משתנים בהתאם למכשיר. לדוגמה, ב-iOS + Chrome וב-iOS + Safari, הקריאה החוזרת מופעלת כל 1/20 שנייה, ואילו ב-Android 4 + Chrome היא מופעלת בערך בכל 1/10 שנייה.

window.addEventListener('deviceorientation', function (e) {
  // do something here..
});

האובייקט DeviceOrientationEvent מכיל נתוני הטיה עבור כל אחד מהצירים X, Y ו-Z במעלות (לא רדיאנים) (מידע נוסף על HTML5Rocks). עם זאת, ערכי ההחזרה משתנים גם בהתאם לשילוב של המכשיר והדפדפן שבהם נעשה שימוש. הטווחים של ערכי ההחזרה בפועל מפורטים בטבלה הבאה:

כיוון המכשיר.

הערכים למעלה שמודגשים בכחול הם הערכים שמוגדרים במפרטי W3C. אלה שמודגשים בירוק תואמים למפרטים האלה, ואילו אלה שמודגשים באדום חורגים מהמפרט. באופן מפתיע, רק השילוב של Android ו-Firefox החזיר ערכים שתאמו למפרטים. עם זאת, כשמדובר בהטמעה, הגיוני יותר לכלול ערכים שמתרחשים לעיתים קרובות. לכן, ב-World Wide Maze משתמשים בערכי ההחזרה של iOS כסטנדרטיים ומתאימים למכשירי Android בהתאם.

if android and event.gamma > 180 then event.gamma -= 360

עם זאת, זה עדיין לא תומך ב-Nexus 10. למרות ש-Nexus 10 מחזיר את אותו טווח ערכים כמו במכשירי Android אחרים, יש באג שהופך את ערכי הבטא והגמא. אנחנו מטפלים בנושא הזה בנפרד. (אולי ברירת המחדל היא פריסה לרוחב?)

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

WebSocket

ב-World Wide Maze, הסמארטפון והמחשב שלך מחוברים באמצעות WebSocket. ליתר דיוק, הם מחוברים ביניהם דרך שרת ממסר, כלומר בין סמארטפון לשרת. הסיבה לכך היא של-WebSocket אין אפשרות לחבר דפדפנים ישירות זה לזה. (השימוש בערוצי נתונים של WebRTC מאפשר קישוריות מקצה לקצה (P2P) ומבטל את הצורך בשרת ממסר, אבל בזמן הטמעת השיטה הזו ניתן להשתמש בה רק עם Chrome Canary ו-Firefox Nightly.)

בחרתי ליישם באמצעות ספרייה בשם Socket.IO (v0.9.11), שכוללת תכונות לחיבור מחדש במקרה של הפסקה זמנית בחיבור או ניתוק. השתמשתי בו יחד עם NodeJS, מכיוון שהשילוב הזה של NodeJS + Socket.IO הראה את הביצועים הטובים ביותר בצד השרת במספר בדיקות הטמעה של WebSocket.

התאמה לפי מספרים

  1. המחשב מתחבר לשרת.
  2. השרת מספק למחשב מספר שנוצר באופן אקראי וזוכר את השילוב של מספר ומחשב.
  3. במכשיר הנייד, מציינים מספר ומתחברים לשרת.
  4. אם המספר שצוין זהה למספר במחשב מחובר, המכשיר הנייד שלכם מותאם למחשב הזה.
  5. אם אין מחשב ייעודי, תתרחש שגיאה.
  6. כשנתונים מגיעים מהמכשיר הנייד, הם נשלחים למחשב שאליו הם מותאמים, ולהפך.

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

סנכרון כרטיסיות

תכונת סנכרון הכרטיסיות הספציפית ל-Chrome מקלה עוד יותר על תהליך ההתאמה. באמצעות רכיב זה, ניתן לפתוח בקלות דפים שפתוחים במחשב (ולהיפך) בנייד. המחשב לוקח את מספר החיבור שהונפק על ידי השרת ומצרף אותו לכתובת ה-URL של דף באמצעות history.replaceState.

history.replaceState(null, null, '/maze/' + connectionNumber)

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

זמן אחזור

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

בהתחלה ציפיתי לבעיות זמן אחזור, ולכן שקלתי להגדיר שרתי ממסר ברחבי העולם כדי שהלקוחות יוכלו להתחבר לשרת הכי קרוב שזמין (וכך לקצר את זמן האחזור). עם זאת, בסופו של דבר השתמשתי ב-Google Compute Engine (GCE), שהיה קיים רק בארה"ב באותו זמן, כך שזה לא היה אפשרי.

הבעיה באלגוריתם נאגל

האלגוריתם של Nagle משולב בדרך כלל במערכות הפעלה לצורך תקשורת יעילה על ידי אגירת נתונים ברמת ה-TCP, אבל גיליתי שלא הצלחתי לשלוח נתונים בזמן אמת כשהאלגוריתם הזה הופעל. (ספציפית בשילוב עם אישור עיכוב TCP. גם אם אין עיכוב ב-ACK, אותה בעיה מתרחשת אם ACK מתעכב במידה מסוימת בגלל גורמים כמו הצבת השרת בחו"ל.)

בעיית זמן האחזור של Nagle לא אירעה ב-WebSocket ב-Chrome ל-Android, הכולל את האפשרות TCP_NODELAY להשבתת Nagle, אך היא התרחשה בעת שימוש ב-WebKit WebSocket המשמש ב-Chrome ל-iOS שבו אפשרות זו אינה מופעלת. (ב-Safari, שמשתמש באותו WebKit, גם הוא נתקל בבעיה הזו. הבעיה דווחה ל-Apple דרך Google ונראה שהיא נפתרה בגרסת הפיתוח של WebKit.

כשהבעיה מתרחשת, נתוני ההטיה שנשלחים כל 100 אלפיות השנייה משולבים במקטעים שמגיעים רק אל המחשב כל 500 אלפיות השנייה. המשחק לא יכול לתפקד בתנאים האלה, ולכן הוא מונע את זמן האחזור הזה בכך שצד השרת שולח נתונים במרווחי זמן קצרים (כל 50 אלפיות שנייה בערך). לדעתי, כשמקבלים ACK במרווחי זמן קצרים, האלגוריתם של Nagle עשוי לחשוב שזה בסדר לשלוח נתונים.

אלגוריתם Nagle 1

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

אלגוריתם Nagle 2

לעומת זאת, בתרשים הזה מוצגות תוצאות השימוש בשרת בארה"ב. מרווחי הפלט הירוקים קבועים במרווחי זמן של 100 אלפיות השנייה, אבל מרווחי הקלט משתנים בין מרווחי זמן נמוכים של 0 אלפיות השנייה לגבוהים של 500 אלפיות השנייה. המשמעות היא שהמחשב מקבל נתונים במקטעים.

ALT_TEXT_HERE

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

באג?

למרות שלדפדפן ברירת המחדל ב-Android 4 (ICS) יש WebSocket API, הוא לא יכול להתחבר וכתוצאה מכך מתקבל אירוע Socket.IO connect_failed. הזמן הקצוב פג באופן פנימי, וגם בצד השרת אין אפשרות לאמת חיבור. (לא בדקתי את זה רק עם WebSocket, אז יכול להיות שמדובר בבעיה ב-Socket.IO).

שרתי ממסר קנה מידה

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

פיזיקה

תנועת הכדור בתוך המשחק (גלגול במדרון, התנגשות עם הקרקע, התנגשות בקירות, איסוף פריטים וכו') מתבצעת באמצעות סימולטור פיזיקה תלת-ממדי. השתמשתי ב-Ammo.js – יציאה של מנוע הפיזיקה הנפוץ Bullet ב-JavaScript באמצעות Emscripten — יחד עם Physijs כדי להשתמש בו כ-"Web Worker".

עובדי אינטרנט

Web Workers הוא ממשק API להרצת JavaScript בשרשורים נפרדים. JavaScript שמופעל כ-Web Worker פועל כשרשור נפרד מה-thread ששמו במקור, כך שניתן לבצע משימות מורכבות תוך שמירה על תגובת הדף. בחברת Physijs משתמשים ביעילות ב-Web Workers כדי לעזור למנוע פיזיקה תלת-ממדי האינטנסיבי, לפעול בצורה חלקה. המבוך ברחבי העולם מטפל במנוע הפיזיקה ובעיבוד התמונה של WebGL בקצבי פריימים שונים לגמרי, לכן גם אם קצב הפריימים יורד במחשב עם מפרט נמוך עקב עומס עיבוד WebGL כבד, מנוע הפיזיקה עצמו ישמור על קצב של 60fps ולא ימנע את פקדי המשחק.

FPS

בתמונה הזו מוצגים קצבי הפריימים שמתקבלים במכשירי Lenovo G570. בתיבה העליונה מוצג קצב הפריימים של WebGL (עיבוד תמונות), ובתיבה התחתונה מוצג קצב הפריימים של מנוע הפיזיקה. ה-GPU הוא שבב Intel HD Graphics 3000, כך שקצב הפריימים של עיבוד התמונה לא הגיע לקצב הצפוי של 60FPS. עם זאת, מכיוון שהמנוע בפיזיקה השיג את קצב הפריימים הצפוי, מהלך המשחק לא שונה כל כך מהביצועים במכשיר עם מפרט גבוה.

מאחר ששרשורים עם Web Workers פעילים אין אובייקטים במסוף, הנתונים חייבים להישלח ל-thread הראשי דרך postMessage כדי להפיק יומני ניפוי באגים. השימוש ב-console4Worker יוצר ב-Worker מקביל לאובייקט מסוף, ולכן תהליך ניפוי הבאגים קל יותר באופן משמעותי.

קובצי שירות (service worker)

הגרסאות האחרונות של Chrome מאפשרות להגדיר נקודות עצירה (breakpoint) בעת השקת Web Workers, שמועילות גם לניפוי באגים. תוכלו למצוא את האפשרות הזו בחלונית Workers בקטע 'כלים למפתחים'.

ביצועים

בשלבים בעלי מספר פוליגונים גבוה, לפעמים יש יותר מ-100,000 פוליגונים, אבל רמת הביצועים לא נפגעה במיוחד, גם כשהם נוצרו במלואם כ-Physijs.ConcaveMesh (btBvhTriangleMeshShape בתבליט).

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

חפצים של רוחות רפאים

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

hit = new Physijs.SphereMesh(geometry, material, 0)
hit._physijs.collision_flags = 1 | 4
scene.add(hit)

עבור collision_flags, 1 הוא CF_STATIC_OBJECT ו-4 הוא CF_NO_CONTACT_RESPONSE. לקבלת מידע נוסף, כדאי לנסות לחפש בפורום של תבליטים, ב-Stack Overflow או בתיעוד של תבליטים. מאחר ש-Pysijs הוא wrapper עבור Ammo.js ו-Amo.js זהה במהותם ל-Bullet, את רוב הדברים שאפשר לעשות בתבליט אפשר לבצע גם ב-Pysijs.

הבעיה ב-Firefox 18

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

asm.js

המצב הזה לא מתייחס ישירות ל-World Wide Maze, אבל ב-Ammmo.js כבר יש תמיכה ב-asm.js של Mozilla שהוצהר לאחרונה (לא מפתיע מפני ש-asm.js נוצרה בעיקר כדי להאיץ את JavaScript שנוצר על ידי Emscripten, והיוצר של Emscripten הוא גם היוצר של Ammo.js). אם Chrome תומך גם ב-asm.js, עומס המחשוב של המנוע הפיזי אמור להצטמצם באופן משמעותי. המהירות הייתה גבוהה יותר באופן משמעותי כשנבדקה עם Firefox Nightly. אולי כדאי לכתוב קטעים שדורשים מהירות גבוהה יותר ב-C/C++ ואז להעביר אותם ל-JavaScript באמצעות Emscripten?

WebGL

לצורך הטמעת WebGL השתמשתי בספרייה שפותחה באופן הפעיל ביותר, three.js (r53). על אף שגרסה 57 כבר פורסמה בשלבי הפיתוח האחרונים, בוצעו שינויים משמעותיים בממשק ה-API, ולכן נשארתי עם הגרסה המקורית לגרסה.

אפקט זוהר

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

Glow

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

כדור מחזיר אור

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

הצללה, תוכנת הצללה, תוכנת הצללה...

טכנולוגיית WebGL מחייבת תוכנות הצללה (shaders) (מוצללים (shaders) וקטעי הצללה של קטעים) לכל הרינדור. תוכנות ההצללה (shader) הכלולות ב-3.js כבר מאפשרות שימוש במגוון רחב של אפקטים, אבל לא ניתן להימנע מכתיבה משלכם כדי לבצע הצללה מורכבת יותר ואופטימיזציה. מכיוון שה-World Wide Maze מעסיק את המעבד (CPU) במנוע הפיזיקה שלו, ניסיתי להשתמש ב-GPU במקום זאת על ידי כתיבה של כמה שיותר תוכן בשפת הצללה (GLSL), גם כשעיבוד המעבד (CPU) (באמצעות JavaScript) היה קל יותר. האפקטים של גלי האוקיינוס מסתמכים באופן טבעי על הזיקוקים בנקודות המטרה, וגם על אפקט הרשת שמשמש כשהכדור מופיע.

כדורי ציל

המידע שהוזכר למעלה הגיע מבדיקות של אפקט הרשת ששימש להצגת הכדור. הסמל בצד שמאל הוא זה שמשמש בתוך המשחק, והוא מורכב מ-320 פוליגונים. זה שנמצא במרכז משתמש בכ-5,000 פוליגונים, והזה שמימין משתמש בכ-300,000 פוליגונים. גם עם כל כך הרבה פוליגונים, עיבוד באמצעות תוכנות הצללה (shader) יכול לשמור על קצב פריימים יציב של 30fps.

רשת הצללה

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

poly2tri

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

poly2tri

למעלה רואים כיצד המתאר הכחול משורטט ופוליגונים אדומים נוצרים.

סינון אניזוטרופי

מכיוון שהמיפוי האיזוטרופי הסטנדרטי של MIP מצמצם את התמונות גם בצירים אופקיים וגם בצירים אנכיים, הצגת פוליגונים מזוויות מוטות יוצרת מרקמים בקצה המרוחק של השלבים במבוך רחב העולם שנראה כמו מרקמים ברזולוציה נמוכה ומוארים אופקית. התמונה השמאלית העליונה בדף הוויקיפדיה הזה מראה דוגמה טובה לכך. בפועל, נדרשת רזולוציה אופקית גבוהה יותר, וטכנולוגיית WebGL (OpenGL) פותרת באמצעות שיטה שנקראת סינון אניזוטרופי. ב-3.js, הגדרת ערך גדול מ-1 עבור THREE.Texture.anisotropy מאפשרת סינון אניזוטרופי. עם זאת, התכונה הזו היא תוסף ויכול להיות שלא כל יחידות ה-GPU יתמכו.

אופטימיזציה

כפי שנזכר גם במאמר השיטות המומלצות של WebGL, הדרך הקריטית ביותר לשיפור הביצועים של WebGL (OpenGL) היא למזער קריאות משיכה. במהלך הפיתוח הראשוני של המבוך העולמי, כל האיים, הגשרים ומעקות השמירה היו חפצים נפרדים. כתוצאה מכך, לפעמים נוצרו יותר מ-2,000 בקשות משיכה, והפעולות שגרמו לביצוע של שלבים מורכבים היו מסובכים. עם זאת, לאחר שדרזתי את אותם סוגי אובייקטים ברשת אחת, הפחתנו את הקריאות ל-50 או פחות, וכתוצאה מכך שיפרנו את הביצועים באופן משמעותי.

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

אופטימיזציה

התוצאות למעלה הן תוצאות מעקב מיצירת מפות סביבה עבור השתקפות הכדור. אם מוסיפים את console.time ו-console.timeEnd למיקומים שלכאורה נחשבים רלוונטיים ב-3.js, נוכל להציג תרשים שנראה כך. הזמן עובר משמאל לימין, וכל שכבה דומה למקבץ קריאות. לאחר סידור של console.time בתוך console.time, אפשר לבצע מדידות נוספות. התרשים העליון מייצג אופטימיזציה מראש, והחלק התחתון הוא לאחר אופטימיזציה. כפי שמוצג בתרשים העליון, בוצעה קריאה לupdateMatrix (למרות שהמילה קטועה) בכל אחת מהרינדור 0-5 במהלך האופטימיזציה מראש. שיניתי אותה כך שהיא תופעל פעם אחת בלבד, מכיוון שהתהליך הזה נדרש רק כאשר אובייקטים משנים את המיקום או הכיוון.

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

הכלי לשינוי ביצועים

בשל אופי האינטרנט, סביר להניח שהמשחק ישוחק במערכות עם מפרטים שונים מאוד. הסרטון Find Your Way to Oz הושק בתחילת פברואר, ובו משתמשים בכיתה IFLAutomaticPerformanceAdjust כדי לצמצם את האפקטים בהתאם לתנודות בקצב הפריימים, כדי להבטיח הפעלה חלקה. המבוך ברחבי העולם מבוסס על אותו מחלקה של IFLAutomaticPerformanceAdjust ומשנה את הגודל של האפקטים לפי הסדר הבא כדי שהגיימפליי יהיה חלק ככל האפשר:

  1. אם קצב הפריימים יורד מתחת ל-45FPS, העדכון של מפות הסביבה נפסק.
  2. אם האיכות שלו עדיין נמוכה מ-40FPS, רזולוציית הרינדור תוקטן ל-70% (50% מיחס פני השטח).
  3. אם הרזולוציה עדיין נמוכה מ-40FPS, מתבצעת הסרה של FXAA (נגד החלקה).
  4. אם האיכות עדיין נמוכה מ-30fps, האפקטים של הזוהר יוסרו.

דליפת זיכרון

סילוק אובייקטים בצורה מסודרת היא הטרחה של שלושה.js. אבל אם הם יישארו ללא שינוי, זה יוביל לדליפות זיכרון, אז פיתחתי את השיטה למטה. @renderer מתייחס אל THREE.WebGLRenderer. (בגרסה האחרונה של שלוש.js נעשה שימוש בשיטה קצת שונה של מיקום דילים, ולכן סביר להניח שהשיטה הזו לא תפעל איתה כפי שהיא).

destructObjects: (object) =>
  switch true
    when object instanceof THREE.Object3D
      @destructObjects(child) for child in object.children
      object.parent?.remove(object)
      object.deallocate()
      object.geometry?.deallocate()
      @renderer.deallocateObject(object)
      object.destruct?(this)

    when object instanceof THREE.Material
      object.deallocate()
      @renderer.deallocateMaterial(object)

    when object instanceof THREE.Texture
      object.deallocate()
      @renderer.deallocateTexture(object)

    when object instanceof THREE.EffectComposer
      @destructObjects(object.copyPass.material)
      object.passes.forEach (pass) =>
        @destructObjects(pass.material) if pass.material
        @renderer.deallocateRenderTarget(pass.renderTarget) if pass.renderTarget
        @renderer.deallocateRenderTarget(pass.renderTarget1) if pass.renderTarget1
        @renderer.deallocateRenderTarget(pass.renderTarget2) if pass.renderTarget2

HTML

באופן אישי, אני חושב שהדבר הכי טוב באפליקציית WebGL הוא היכולת לעצב את פריסת הדפים ב-HTML. בניית ממשקים דו-ממדיים, כגון ציון או תצוגות טקסט ב-Flash או ב-openFrameworks (OpenGL) היא די מסובכת. ל-Flash יש לפחות סביבת פיתוח משולבת (IDE), אבל OpenFrameworks קשה יותר אם לא רגילים אליו (שימוש ב-Cocos2D עשוי להקל על התהליך). HTML, לעומת זאת, מאפשר שליטה מדויקת בכל ההיבטים של עיצוב הקצה עם CSS, בדיוק כמו בעת בניית אתרים. למרות שלא ניתן להשתמש באפקטים מורכבים כמו חלקיקים שמתרכזים בלוגו, אפשר להשתמש בחלק מהאפקטים התלת-ממדיים במסגרת היכולות של הטרנספורמציות של CSS. אפקטים של טקסט מסוג "GOAL" ו-"TIME IS UP" של World Wide Maze מונפשים באמצעות קנה מידה במעבר CSS (מוטמע באמצעות Transit). (כמובן שההדרגות ברקע משתמשות ב-WebGL).

לכל דף במשחק (הכותרת, RESULT, 'דירוג' וכו') יש קובץ HTML משלו, ולאחר שהדפים נטענים כתבניות, $(document.body).append() מופעל עם הערכים המתאימים בזמן המתאים. בעיה אחת הייתה שלא ניתן להגדיר אירועי עכבר ומקלדת לפני ההוספה, ולכן הניסיון el.click (e) -> console.log(e) לפני הצירוף לא עבד.

אינטרנציונליזציה (i18n)

העבודה ב-HTML הייתה נוחה גם ליצירת הגרסה בשפה האנגלית. בחרתי להשתמש ב-i18next, ספריית אינטרנט ב-i18n, לצורכי הבינלאומיות שלי, שהצלחתי להשתמש בה ללא שינוי.

עריכה ותרגום של טקסט בתוך המשחק בוצעו בגיליון האלקטרוני של Google Docs. מאחר ש-i18next דורשת קובצי JSON, ייצאתי את הגיליונות האלקטרוניים ל-TSV ולאחר מכן המרתי אותם באמצעות ממיר מותאם אישית. ביצעתי הרבה עדכונים ממש לפני ההשקה, לכן אוטומציה של תהליך הייצוא מגיליון אלקטרוני של Google Docs הייתה הרבה יותר קלה.

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

RequireJS

בחרתי את RequireJS כמערכת של מודול ה-JavaScript. 10,000 השורות בקוד המקור של המשחק מחולקות לכ-60 מחלקות (= קובצי קפה) ועוברות הידור לקובצי js נפרדים. RequireJS טוען את הקבצים הנפרדים האלו בסדר מתאים, בהתאם ליחסים.

define ->
  class Hoge
    hogeMethod: ->

ניתן להשתמש במחלקה שהוגדרה למעלה (hoge.coffee) באופן הבא:

define ['hoge'], (Hoge) ->
  class Moge
    constructor: ->
      @hoge = new Hoge()
      @hoge.hogeMethod()

כדי לעבוד, צריך לטעון את hoge.js לפני moge.js, ומכיוון ש-"hoge" מוגדר כארגומנט הראשון של "define" , hoge.js תמיד נטען ראשון (נקרא חזרה אחרי שמסתיימת הטעינה של hoge.js). המנגנון הזה נקרא AMD, ואפשר להשתמש בכל ספרייה של צד שלישי לאותו סוג של קריאה חוזרת (callback), כל עוד הוא תומך ב-AMD. גם אלו שלא (למשל, 3.js) יניבו ביצועים דומים כל עוד שנקבעו מראש.

הדבר דומה לייבוא AS3, כך שזה לא אמור להיראות מוזר. אם יהיו לכם יותר קבצים תלויים, זה פתרון אפשרי.

r.js

RequireJS כולל כלי אופטימיזציה בשם r.js. הפעולה הזו מאגדת את ה-js הראשי עם כל קובצי ה-js התלויים בתוך אחד, ולאחר מכן מקטינה אותו באמצעות UglifyJS (או מהדר סגירה). הפעולה הזו מפחיתה את מספר הקבצים ואת כמות הנתונים שהדפדפן צריך לטעון. הגודל הכולל של קובץ ה-JavaScript ב-World Wide Maze הוא כ-2MB וניתן להקטין אותו לכ-1MB באמצעות אופטימיזציית r.js. אם ניתן היה להפיץ את המשחק באמצעות gzip, הערך צומצם עוד יותר ל-250KB. (ב-GAE יש בעיה שלא מאפשרת העברה של קובצי gzip בנפח של 1MB או יותר, לכן המשחק מופץ כרגע ללא דחיסה ובגודל 1MB של טקסט פשוט).

בניית שלבים

נתוני השלבים נוצרים באופן הבא ומבוצעים אך ורק בשרת GCE בארה"ב:

  1. כתובת ה-URL של האתר שיש להמיר לשלב נשלחת באמצעות WebSocket.
  2. PhantomJS מצלם צילום מסך, ומיקומי תגי div ו-img מאוחזרים ויוצרים פלט בפורמט JSON.
  3. על סמך צילום המסך משלב 2 ונתוני המיקום של רכיבי HTML, תוכנית מותאמת אישית של C++ (OpenCV, Boost) מוחקת אזורים מיותרים, יוצרת איים, מחברת את האיים באמצעות גשרים, מחשבת את מיקומי הפריטים ומגדירה את נקודת היעד וכו'. התוצאות מתקבלות בפורמט JSON ומוחזרות לדפדפן.

PhantomJS

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

עם PhantomJS, JavaScript או ContactScript משמש לכתיבת התהליכים שברצונך לבצע. קל מאוד לצלם צילומי מסך, כפי שמתואר בדוגמה הזו. עבדתי על שרת Linux (CentOS), והייתי צריך להתקין גופנים כדי להציג יפנית (M+ FontS). גם במקרה כזה, עיבוד הגופנים מטופל באופן שונה ב-Windows או ב-Mac OS, לכן אותו גופן עשוי להיראות שונה במחשבים אחרים (עם זאת, ההבדל הוא מזערי).

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

stage_builder

בהתחלה שקלתי להשתמש בגישה מבוססת DOM כדי ליצור שלבים (בדומה ל-Firefox 3D Inspector) וניסיתי משהו כמו ניתוח DOM ב-PantomJS. אבל בסופו של דבר בחרתי בגישה של עיבוד תמונה. לשם כך כתבתי תוכנית C++ שמשתמשת ב-OpenCV וב-Boost בשם "stage_builder". הוא מבצע את הפעולות הבאות:

  1. מתבצעת טעינה של צילום המסך וקובצי ה-JSON.
  2. ממירה תמונות וטקסט ל "איים".
  3. יצירת גשרים לחיבור בין האיים.
  4. חוסכת גשרים מיותרים ליצירת מבוך.
  5. ממקמים פריטים גדולים.
  6. מציבים פריטים קטנים.
  7. מסילות שמירה.
  8. מיקום פלט של נתוני מיקום בפורמט JSON.

כל אחד מהשלבים מפורט בהמשך.

מתבצעת טעינה של צילום המסך וקובצי ה-JSON

המערכת משתמשת בcv::imread הרגילים כדי לטעון צילומי מסך. בדקתי כמה ספריות לקובצי ה-JSON, אבל נראה שהכי קל לעבוד עם picojson.

המרת תמונות וטקסט ל "איים"

בניית שלב

למעלה הוא צילום מסך של הקטע בחדשות Google באתר aid-dcc.com (ניתן ללחוץ כדי להציג את הגודל האמיתי). צריך להמיר את התמונות והטקסט לאיים. כדי לבודד את הקטעים האלה, עלינו למחוק את צבע הרקע הלבן, במילים אחרות, את הצבע הנפוץ ביותר בצילום המסך. כך זה נראה לאחר ביצוע השינוי:

בניית שלב

החלקים הלבנים הם האיים הפוטנציאליים.

הטקסט דק וחד מדי, לכן נעים אותו באמצעות cv::dilate, cv::GaussianBlur ו-cv::threshold. גם תוכן התמונה חסר, לכן נמלא את האזורים האלה בלבן, על סמך פלט נתוני תג ה-img מ-PantomJS. התמונה שמתקבלת נראית כך:

בניית שלב

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

יצירת גשרים לחיבור בין האיים

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

בניית שלב

הסרת גשרים מיותרים ליצירת מבוך

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

בניית שלב

הנחת פריטים גדולים

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

בניית שלב

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

הצבת פריטים קטנים

בניית שלב

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

הצבת מסילות הגנה

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

בניית שלב

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

פלט נתוני מיקום בפורמט JSON

השתמשתי גם ב-picojson בפלט. הוא כותב את הנתונים בפלט סטנדרטי, שלאחר מכן מתקבל על ידי מבצע הקריאה (Node.js).

יצירת תוכנית C++ ב-Mac להפעלה ב-Linux

המשחק פותח ב-Mac ונפרס ב-Linux, אבל מכיוון ש-OpenCV ו-Boost היו קיימים לשתי מערכות ההפעלה, לא היה קשה לפתח את סביבת ההידור עצמו. השתמשתי בכלי שורת הפקודה ב-Xcode כדי לנפות באגים ב-build ב-Mac, ואז יצרתי קובץ תצורה באמצעות automake/autoconf כדי שאפשר יהיה להדר את ה-build ב-Linux. אז הייתי צריך פשוט להשתמש ב-"Configure && Make" ב-Linux כדי ליצור את קובץ ההפעלה. נתקלתי בכמה באגים ספציפיים ל-Linux עקב הבדלים בגרסאות המהדר, אבל הצלחתי לפתור אותם בקלות יחסית באמצעות gdb.

סיכום

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