מידע על עיבוד בלופ במשחקים

דרך פופולרית מאוד להטמיע לולאת משחק נראית כך:

while (playing) {
    advance state by one frame
    render the new frame
    sleep until it’s time to do the next frame
}

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

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

  • שימוש בספרייה של Android Framework (מומלץ)
  • למלא את כל מאגר מאגר הנתונים הזמני ולהסתמך על 'מאגרי נתונים זמניים' לחץ גב
  • שימוש בכוריאוגרף (API מגרסה 16 ואילך)

ספרייה של קצב פריימים ב-Android

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

דחיסת פריטים בתור

קל מאוד ליישם את זה: פשוט החליפו מאגרי נתונים זמניים במהירות האפשרית. מוקדם בגרסאות של Android, הדבר עלול להוביל לנקיטת עונש כאשר האפליקציה SurfaceView#lockCanvas() תעביר אותך למצב שינה במשך 100 אלפיות השנייה. היום הוא מקבל את הקצב של BufferQueue, וה-BuffQueue מתרוקן באותה מהירות יש אפשרות ל-SurfaceFlinger.

דוגמה אחת לגישה הזו מוצגת בקטע Android Breakout. הוא משתמש ב-GLSurfaceView, שרץ בלולאה שקוראת לאפליקציה onDrawFrame() ולאחר מכן מחליף את מאגר הנתונים הזמני. אם BufferQueue הוא מלא, השיחה eglSwapBuffers() תמתין עד שמאגר הנתונים הזמני יהיה זמין. מאגרי הנתונים הזמניים הופכים לזמינים כש-SurfaceFlinger משיק אותם, ואז הוא עושה זאת רכישת לקוח חדש לתצוגה. מכיוון שזה קורה ב-VSYNC, לולאת השרטוט שלכם התזמון יתאים לקצב הרענון. בעיקר.

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

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

Choreographer

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

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

אפליקציית "Record GL" לפעילות ב-Grafika אפשר לראות דוגמה לכך. בחלק מכשירים (למשל Nexus 4 ו-Nexus 5), הפעילות תתחיל לרדת פריימים אם פשוט לשבת ולצפות. עיבוד ה-GL הוא טריוויאלי, אבל לפעמים ה-View והרכיבים משורטטים מחדש, ומעבר המדידה/הפריסה עשוי להימשך זמן רב מאוד המכשיר עבר למצב של הספק מופחת. (לפי המערכת של systrace, ב-Android 4.4 לוקח 28 אלפיות שנייה במקום 6 אלפיות שנייה. אם גוררים את האצבע על המסך, היא חושבת שאתם מבצעים אינטראקציה עם הפעילות, כך שמהירות השעון נשארת גבוהה ואף פעם לא נופלת פריים).

הפתרון הפשוט היה להסיר פריים בקריאה החוזרת של הכוריאוגרף, אם הוא יותר מ-N אלפיות שנייה אחרי הזמן של VSYNC. הערך האידיאלי של N נקבע על סמך מרווחים של VSYNC שתועדו בעבר. לדוגמה, אם תקופת הרענון היא 16.7 אלפיות השנייה (60fps). אם רצים יותר, יכול להיות שהפריים יושמט באיחור של יותר מ-15 אלפיות השנייה.

אם צופים ב"אפליקציית Record GL" תראו את המונה שנפטר, גדלה ואפילו לראות הבהוב של אדום בגבול כשהמסגרות נופלות. אלא אם אבל העיניים שלכם מאוד טובות, אבל לא תוכלו לראות את האנימציה מקוטעת. ב-60fps, האפליקציה יכולה להסיר את הפריים מדי פעם בלי שאף אחד ישב לב כל עוד האנימציה ממשיכה להתקדם בקצב קבוע. כמה אפשר להתחמק תלויים במידה מסוימת במה שאתם משרטטים, במאפיינים של ועד כמה טוב לאדם שמשתמש באפליקציה באיתור בעיות.

ניהול שרשורים

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

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

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

run() {
    Thread.sleep(100);
    synchronized (mLock) {
        moveBlock();
    }
}

(כדאי לבסס את זמן השינה על שעון קבוע כדי למנוע סחף - ה-שינה() לא עקבית לגמרי, ו-moveBlock() לוקח כמות גדולה יותר מ-0 אבל הבנת את הרעיון.)

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

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