Unity ייעל את המשחק שלך באמצעות Profiler
ביצועים הם היבט מרכזי של כל משחק ואין הפתעה, לא משנה כמה טוב המשחק, אם הוא פועל בצורה גרועה על המחשב של המשתמש, זה לא ירגיש מהנה.
מכיוון שלא לכולם יש מחשב או מכשיר מתקדמים (אם אתה מכוון לנייד), חשוב לזכור את הביצועים במהלך כל מהלך הפיתוח.
ישנן מספר סיבות לכך שהמשחק יכול לרוץ לאט:
- עיבוד (יותר מדי רשתות פולי גבוהות, הצללות מורכבות או אפקטים של תמונה)
- אודיו (בעיקר נגרמת על ידי הגדרות ייבוא שמע שגויות)
- קוד לא מותאם (סקריפטים המכילים פונקציות תובעות ביצועים במקומות הלא נכונים)
במדריך זה, אני אראה כיצד לבצע אופטימיזציה של הקוד שלך בעזרת Unity Profiler.
מאפיין
מבחינה היסטורית, ביצועי ניפוי באגים ב-Unity היו משימה מייגעת, אבל מאז, נוספה תכונה חדשה, הנקראת Profiler.
Profiler הוא כלי ב-Unity המאפשר לך לאתר במהירות את צווארי הבקבוק במשחק שלך על ידי ניטור צריכת הזיכרון, מה שמפשט מאוד את תהליך האופטימיזציה.
ביצועים רעים
ביצועים גרועים יכולים לקרות בכל עת: נניח שאתה עובד על מופע האויב וכאשר אתה מציב אותו בסצנה, זה עובד מצוין בלי שום בעיות, אבל ככל שאתה מוליד יותר אויבים אתה עלול להבחין ב-fps (פריימים לשנייה) ) מתחילים לרדת.
בדוק את הדוגמה למטה:
בסצנה, יש לי קובייה עם סקריפט מצורף אליה, שמזיז את הקוביה מצד לצד ומציג את שם האובייקט:
SC_ShowName.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SC_ShowName : MonoBehaviour
{
bool moveLeft = true;
float movedDistance = 0;
// Start is called before the first frame update
void Start()
{
moveLeft = Random.Range(0, 10) > 5;
}
// Update is called once per frame
void Update()
{
//Move left and right in ping-pong fashion
if (moveLeft)
{
if(movedDistance > -2)
{
movedDistance -= Time.deltaTime;
Vector3 currentPosition = transform.position;
currentPosition.x -= Time.deltaTime;
transform.position = currentPosition;
}
else
{
moveLeft = false;
}
}
else
{
if (movedDistance < 2)
{
movedDistance += Time.deltaTime;
Vector3 currentPosition = transform.position;
currentPosition.x += Time.deltaTime;
transform.position = currentPosition;
}
else
{
moveLeft = true;
}
}
}
void OnGUI()
{
//Show object name on screen
Camera mainCamera = Camera.main;
Vector2 screenPos = mainCamera.WorldToScreenPoint(transform.position + new Vector3(0, 1, 0));
GUI.color = Color.green;
GUI.Label(new Rect(screenPos.x - 150/2, Screen.height - screenPos.y, 150, 25), gameObject.name);
}
}
בהסתכלות על הנתונים הסטטיסטיים, אנו יכולים לראות שהמשחק פועל במהירות של 800+ פריימים לשנייה, כך שבקושי יש לו השפעה על הביצועים.
אבל בואו נראה מה יקרה כשנשכפל את הקוביה 100 פעמים:
FPS ירד ביותר מ-700 נקודות!
הערה: כל הבדיקות נעשו כאשר Vsync מושבת
באופן כללי, מומלץ להתחיל לבצע אופטימיזציה כאשר המשחק מתחיל להפגין גמגום, קפוא או שה-fps יורד מתחת ל-120.
כיצד להשתמש ב-Profiler?
כדי להתחיל להשתמש ב-Profiler תצטרך:
- התחל את המשחק שלך על ידי לחיצה על Play
- פתח את Profiler על ידי מעבר לחלון -> ניתוח -> Profiler (או הקש Ctrl + 7)
- יופיע חלון חדש שנראה בערך כך:
- זה אולי נראה מאיים בהתחלה (במיוחד עם כל התרשימים האלה וכו'), אבל זה לא החלק שנסתכל עליו.
- לחץ על הכרטיסייה ציר זמן ושנה אותה להיררכיה:
- תבחין ב-3 קטעים (EditorLoop, PlayerLoop ו- Profiler.CollectEditorStats):
- הרחב את ה-PlayerLoop כדי לראות את כל החלקים שבהם מושקע כוח החישוב (הערה: אם ערכי ה-PlayerLoop אינם מתעדכנים, לחץ על כפתור "Clear" בחלק העליון של חלון Profiler).
לקבלת התוצאות הטובות ביותר, כוון את דמות המשחק שלך למצב (או למקום) בו המשחק מפגר הכי הרבה והמתן כמה שניות.
- לאחר המתנה מעט, עצור את המשחק וצפה ברשימת PlayerLoop
אתה צריך להסתכל על הערך GC Alloc, המייצג את הקצאת איסוף זבל. זהו סוג של זיכרון שהוקצה על ידי רכיב אך אין בו עוד צורך ומחכה להשתחרר על ידי ה-Garbage Collection. באופן אידיאלי, הקוד לא אמור ליצור זבל כלשהו (או להיות קרוב ל-0 ככל האפשר).
זמן ms הוא גם ערך חשוב, הוא מראה כמה זמן לקח לקוד לרוץ באלפיות שניות, כך שבאופן אידיאלי, כדאי לשאוף להקטין גם את הערך הזה (על ידי שמירת ערכים במטמון, הימנעות מקריאת פונקציות הדורשות ביצועים בכל עדכון וכו'.).
כדי לאתר את החלקים הבעייתיים מהר יותר, לחץ על העמודה GC Alloc כדי למיין את הערכים מגבוה לנמוך יותר)
- בתרשים השימוש במעבד לחץ במקום כלשהו כדי לדלג למסגרת זו. באופן ספציפי, עלינו להסתכל על פסגות, בהן ה-fps היה הנמוך ביותר:
הנה מה שחשף הפרופיל:
GUI.Repaint מקצה 45.4KB, שזה די הרבה, והרחבתו חשפה מידע נוסף:
- זה מראה שרוב ההקצאות מגיעות משיטת GUIUtility.BeginGUI() ו-OnGUI() בסקריפט SC_ShowName, בידיעה שאנחנו יכולים להתחיל לבצע אופטימיזציה.
GUIUtility.BeginGUI() מייצג שיטת OnGUI() ריקה (כן, אפילו השיטה הריקה OnGUI() מקצה די הרבה זיכרון).
השתמש בגוגל (או במנוע חיפוש אחר) כדי למצוא את השמות שאתה לא מזהה.
להלן החלק OnGUI() שצריך לבצע אופטימיזציה:
void OnGUI()
{
//Show object name on screen
Camera mainCamera = Camera.main;
Vector2 screenPos = mainCamera.WorldToScreenPoint(transform.position + new Vector3(0, 1, 0));
GUI.color = Color.green;
GUI.Label(new Rect(screenPos.x - 150/2, Screen.height - screenPos.y, 150, 25), gameObject.name);
}
אופטימיזציה
בואו נתחיל לבצע אופטימיזציה.
כל סקריפט SC_ShowName קורא לשיטת OnGUI() משלו, וזה לא טוב בהתחשב שיש לנו 100 מופעים. אז מה אפשר לעשות בנידון? התשובה היא: שיהיה לך סקריפט בודד עם שיטת OnGUI() שקוראת לשיטת GUI עבור כל Cube.
- ראשית, החלפתי את ברירת המחדל OnGUI() בסקריפט SC_ShowName ב- public void GUImethod() שייקרא מסקריפט אחר:
public void GUIMethod()
{
//Show object name on screen
Camera mainCamera = Camera.main;
Vector2 screenPos = mainCamera.WorldToScreenPoint(transform.position + new Vector3(0, 1, 0));
GUI.color = Color.green;
GUI.Label(new Rect(screenPos.x - 150/2, Screen.height - screenPos.y, 150, 25), gameObject.name);
}
- לאחר מכן יצרתי סקריפט חדש וקראתי לו SC_GUIMethod:
SC_GUIMethod.cs
using UnityEngine;
public class SC_GUIMethod : MonoBehaviour
{
SC_ShowName[] instances; //All instances where GUI method will be called
void Start()
{
//Find all instances
instances = FindObjectsOfType<SC_ShowName>();
}
void OnGUI()
{
for(int i = 0; i < instances.Length; i++)
{
instances[i].GUIMethod();
}
}
}
SC_GUIMethod יצורף לאובייקט אקראי בסצנה ותקרא לכל שיטות ה-GUI.
- עברנו מ-100 שיטות OnGUI() אינדיבידואליות לשיטות אחת בלבד, בואו נלחץ על play ונראה את התוצאה:
- GUIUtility.BeginGUI() מקצה כעת רק 368B במקום 36.7KB, הפחתה גדולה!
עם זאת, שיטת OnGUI() עדיין מקצה זיכרון, אך מכיוון שאנו יודעים שהיא קוראת רק ל-GUIMethod() מהסקריפט SC_ShowName, אנו עוברים ישר לאיפוי באגים בשיטה זו.
אבל הפרופיל מציג רק מידע גלובלי, איך אנחנו רואים מה בדיוק קורה בתוך השיטה?
כדי לבצע ניפוי באגים בתוך השיטה, ל-Unity יש API שימושי בשם Profiler.BeginSample
Profiler.BeginSample מאפשר לך ללכוד קטע מסוים של הסקריפט, מראה כמה זמן לקח להשלים וכמה זיכרון הוקצה.
- לפני השימוש במחלקה Profiler בקוד, עלינו לייבא את מרחב השמות UnityEngine.Profiling בתחילת הסקריפט:
using UnityEngine.Profiling;
- ה-Profiler Sample נקלט על ידי הוספת Profiler.BeginSample("SOME_NAME"); בתחילת הלכידה והוספת Profiler.EndSample(); בסוף הלכידה, כמו זֶה:
Profiler.BeginSample("SOME_CODE");
//...your code goes here
Profiler.EndSample();
מכיוון שאני לא יודע איזה חלק ב-GUIMethod() גורם להקצאות זיכרון, צירפתי כל שורה ב- Profiler.BeginSample ו- Profiler.EndSample (אבל אם לשיטה שלך יש הרבה שורות, אתה בהחלט לא צריך לצרף כל שורה, פשוט חלקו אותו לחלקים אחידים ואז עבדו משם).
הנה שיטה אחרונה עם יישום דגימות Profiler:
public void GUIMethod()
{
//Show object name on screen
Profiler.BeginSample("sc_show_name part 1");
Camera mainCamera = Camera.main;
Profiler.EndSample();
Profiler.BeginSample("sc_show_name part 2");
Vector2 screenPos = mainCamera.WorldToScreenPoint(transform.position + new Vector3(0, 1, 0));
Profiler.EndSample();
Profiler.BeginSample("sc_show_name part 3");
GUI.color = Color.green;
GUI.Label(new Rect(screenPos.x - 150/2, Screen.height - screenPos.y, 150, 25), gameObject.name);
Profiler.EndSample();
}
- עכשיו אני לוחץ על הפעל וראה מה זה מראה בפרופילי:
- מטעמי נוחות, חיפשתי "sc_show_" בפרופילר, מכיוון שכל הדוגמאות מתחילות בשם זה.
- מעניין... הרבה זיכרון מוקצה בחלק 3 של sc_show_names, שמתאים לחלק הזה של הקוד:
GUI.color = Color.green;
GUI.Label(new Rect(screenPos.x - 150/2, Screen.height - screenPos.y, 150, 25), gameObject.name);
לאחר כמה חיפושים בגוגל, גיליתי שקבלת השם של Object מקצה די הרבה זיכרון. הפתרון הוא להקצות שם של אובייקט למשתנה מחרוזת ב-void Start(), כך הוא ייקרא רק פעם אחת.
הנה הקוד המותאם:
SC_ShowName.cs
using UnityEngine;
using UnityEngine.Profiling;
public class SC_ShowName : MonoBehaviour
{
bool moveLeft = true;
float movedDistance = 0;
string objectName = "";
// Start is called before the first frame update
void Start()
{
moveLeft = Random.Range(0, 10) > 5;
objectName = gameObject.name; //Store Object name to a variable
}
// Update is called once per frame
void Update()
{
//Move left and right in ping-pong fashion
if (moveLeft)
{
if(movedDistance > -2)
{
movedDistance -= Time.deltaTime;
Vector3 currentPosition = transform.position;
currentPosition.x -= Time.deltaTime;
transform.position = currentPosition;
}
else
{
moveLeft = false;
}
}
else
{
if (movedDistance < 2)
{
movedDistance += Time.deltaTime;
Vector3 currentPosition = transform.position;
currentPosition.x += Time.deltaTime;
transform.position = currentPosition;
}
else
{
moveLeft = true;
}
}
}
public void GUIMethod()
{
//Show object name on screen
Profiler.BeginSample("sc_show_name part 1");
Camera mainCamera = Camera.main;
Profiler.EndSample();
Profiler.BeginSample("sc_show_name part 2");
Vector2 screenPos = mainCamera.WorldToScreenPoint(transform.position + new Vector3(0, 1, 0));
Profiler.EndSample();
Profiler.BeginSample("sc_show_name part 3");
GUI.color = Color.green;
GUI.Label(new Rect(screenPos.x - 150/2, Screen.height - screenPos.y, 150, 25), objectName);
Profiler.EndSample();
}
}
- בוא נראה מה הפרופיל מציג:
כל הדגימות מקצות 0B, כך שלא מוקצה עוד זיכרון.