שלום, קוראים לי סרגיי קאצ'אן, ואני מפתחת לקוח בפרויקט War Robots. רובוטים מלחמה כבר שם במשך שנים רבות, ובמהלך הזמן הזה המשחק צבר מגוון עצום של תוכן: רובוטים, נשק, דרונים, טיטנים, טייסים, וכן הלאה. היום אני הולך לדבר על איך האיזונים מאורגנים בפרויקט שלנו, מה קרה להם במהלך 11 השנים האחרונות, וכיצד התמודדנו עם זה. איזון בפרויקט כמו כל פרויקט אחר, רובוטי מלחמה ניתן לחלק לשני חלקים: מטא והמשחק הליבה. כל פעילות שמתרחשת מעבר לגלגל המשחק, אך עדיין משפיעה על המשחק, כולל רכישת תוכן משחק, השתתפות בפעילויות חברתיות או באירועים. Meta gameplay (metagaming) הוא מחזור החוזר העיקרי של פעולות שהשחקן מבצע במשחק כדי להשיג את המטרות שלהם. Core gameplay (core gameplay loop) כל חלק של הפרויקט זקוק לאיזון משלו, ולכן גם אנו מחלקים את האיזון לשתי קטגוריות - מטא והבסיס. רובוטי המלחמה נקראים גם אשר דורשים איזון משלהם. Skirmish modes א זהו שינוי של מצבים קיימים או מפות עם מאפיינים או כללים שונים.מצבים Skirmish הם לעתים קרובות מבוססים על אירועים, זמינים לשחקנים במהלך חגים שונים, בעיקר בשביל הכיף.לדוגמה, שחקנים עשויים להיות מסוגלים להרוג אחד את השני עם יורה אחד או לנוע סביב במשקל אפס. Skirmish mode אז בסך הכל, יש לנו 4 שוויון: 2 עבור מצב ברירת המחדל ו 2 עבור מצב Skirmish. במהלך 11 השנים, War Robots צבר טון של תוכן מדהים: 95 רובוטים 21 טיטנים 175 כלי נשק שונים 40 דרונים 16 אמהות מספר עצום של עור, שינויים, מודולים, טייסים, מגדלים, גרסאות אחרונות של תוכן ומפות וכפי שאתם יכולים לדמיין, כדי לעשות את כל העבודה הזאת אנחנו צריכים לאחסן מידע על התנהגות, סטטיסטיקה, זמינות, מחירים, ועוד הרבה, הרבה יותר. כתוצאה מכך, האיזונים שלנו גדלו לגודל לא הוגן: Default mode Skirmish mode Meta balance 9.2 MB 9.2 MB Core balance 13.1 MB 13.1 MB Meta balance 9.2 MB 9.2 MB Core balance 13.1 MB 13.1 MB לאחר כמה חישובים מהירים, מצאנו כי שחקן צריך להוריד זה די הרבה! 44.6 MB לא רצינו לגרום לשחקנים להוריד כמויות גדולות של נתונים בכל פעם ששינו את המשקל. רק כדי להזכיר לכם: רובוטים מלחמה הגיעו בשנת 2024, הקהל הפעיל החודשי שלנו היה , ו נרשם בכל יום. 300 million registered users 4.7 million people 690 thousand players עכשיו לדמיין את כמות הנתונים. הרבה, נכון? חשבנו כך גם כן. אז, החלטנו לעשות כל מה שביכולתנו כדי להפחית את גודל המשקל שלנו! לרדוף אחרי הבעיה הצעד הראשון היה לנתח את המשוואות ולנסות להבין: "מה לוקח כל כך הרבה מקום?" העברת הכל ידנית הייתה הדבר האחרון שרצינו לעשות - זה לקח שנים, אז כתבנו קבוצה של כלים שאספנו ואספנו את כל המידע הדרוש לנו על המשקל. הכלי ייקח קובץ איזון ככניסה ו, באמצעות השתקפות, יחזור דרך כל המבנים, אוסף נתונים על אילו סוגים של מידע שאנו מאחסנים וכמה מקום כל אחד עסק. התוצאות היו מעודדות: מטרה איזון % in balance Usage count String 28.478 % 164 553 Int32 27.917 % 161 312 Boolean 6.329 % 36 568 Double 5.845 % 33 772 Int64 4.682 % 27 054 Custom structures 26.749 % — String 28,478 אחוז 164 553 Int32 27 917 אחוז 161 312 Boolean 6.329 אחוזים 36 568 Double 5.845 אחוז 33 772 Int64 4,682 אחוזים 27 054 ש"ח Custom structures 26749 אחוזים — האיזון העיקרי % in balance Usage count String 34.259 % 232 229 Double 23.370 % 158 418 Int32 20.955 % 142 050 Boolean 5.306 % 34 323 Custom structures 16.11 % — String 34259 אחוזים 232 229 Double 23,370 אחוזים 158 418 Int32 20955 אחוזים 142 050 Boolean 5306 אחוזים 34 323 Custom structures 16.11 אחוזים — לאחר ניתוח המצב, הבנו כי וכך היה צריך לעשות משהו בעניין. strings were taking up far too much space זה סורק את קובץ האיזון ויצר מפה של כל החוטים יחד עם מספר פעמים כל אחד היה כפול. התוצאות לא היו מעודדות גם כן.חלק מן החוטים חוזרו עשרות אלפי פעמים! מצאנו את הבעיה, ועכשיו השאלה היא: איך נפתור אותה? אופטימיזציה של האיזון מסיבות ברורות, לא יכולנו פשוט להיפטר מחרוזות לגמרי. חרוזים משמשים לדברים כמו מפתחות מיקום וזיהויים שונים. הרעיון היה פשוט ככל שיהיה: צור רשימה של שורות ייחודיות עבור כל איזון (בבסיס, אחסון ייעודי). שלח את הרשימה הזו יחד עם הנתונים. public class BalanceMessage { public BalanceMessageData Data; public StringStorage Storage; public string Version; } StringStorage הוא בעצם מכסה סביב רשימה של חרוזים.כאשר אנו בונים את אחסון החרוזים, כל מבנה איזון זוכר את האינדקס של החרוזים שהוא צריך.אחר כך, בעת חיפוש נתונים, אנחנו פשוט להעביר את האינדקס ולקבל במהירות את הערך. public class StringStorage { public List<string> Values; public string GetValue(StringIdx id) => Values[id]; } במקום להעביר את החוטים עצמם בתוך מבני האיזון, התחלנו להעביר את האינדקס של איפה החוט מאוחסן באחסון החוט. לפני : public class SomeBalanceMessage { public string Id; public string Name; public int Amount; } לאחר : public class SomeBalanceMessageV2 { public StringIdx Id; public StringIdx Name; public int Amount; } StringIdx הוא בעצם רק חבילה סביב int. בדרך זו, אנחנו לחלוטין לחסל העברות שורות ישירות בתוך מבני האיזון. public readonly struct StringIdx : IEquatable<StringIdx> { private readonly int _id; internal StringIdx(int value) {_id = value; } public static implicit operator int(StringIdx value) => value._id; public bool Equals(StringIdx other) => _id == other._id; } גישה זו צמצמה את מספר החוטים בעשרות פעמים. String usage count String usage count Before After Meta balance 164 553 10 082 Core balance 232 229 14 228 Before After Meta balance 164 553 10 082 ש"ח Core balance 232 229 14 228 לא רע נכון? אבל זה היה רק ההתחלה - לא הפסקנו שם. מחדש את פרוטוקול הנתונים כדי להעביר ולעבד מבנים של איזון, היינו משתמשים . MessagePack MessagePack הוא פורמט סדרת נתונים בינארי שנועד להיות חלופה קומפקטית ומהירה יותר ל- JSON.הוא מיועד לחילופי נתונים יעילים בין יישומים או שירותים, המאפשרים הפחתה משמעותית בגודל הנתונים - שימושי במיוחד כאשר ביצועים ורוחב הטווח חשובים. בתחילה, MessagePack הגיע בפורמט דומה JSON, שבו הנתונים המשמשים זה בהחלט נוח, אבל גם די לוקח מקום, אז החלטנו להקריב קצת גמישות ולהחליף . string keys binary byte array לפני : public class SomeBalanceMessage { [Key("id")] public string Id; [Key("name")] public string Name; [Key("amount")] public int Amount; } לאחר : public class SomeBalanceMessageV2 { [Key(0)] public StringIdx Id; [Key(1)] public StringIdx Name; [Key(2)] public int Amount; } כמו כן הסרנו את כל האוספים הריקים – במקום לשלוח אותם, אנו מעבירים כעת ערכים null, מה שהפחית את גודל הנתונים הכולל ואת הזמן הדרוש לסריליזציה ודיסריאליזציה. בדיקת השינויים חוק הזהב של פיתוח טוב (ואחד שישמור לך הרבה עצבים) הוא תמיד ליישם תכונות חדשות בדרך המאפשרת לך לשחזר אותן במהירות אם קורה משהו לא בסדר. במהלך פיתוח, היינו צריכים לוודא שכל הנתונים הועברו כראוי. משאבים ישנים וחדשים – ללא קשר לפורמט או למבנה – היו צריכים לייצר את אותם ערכים בדיוק.זכור, המשאבים האופטימליים שינו את המבנה שלהם באופן דרמטי, אבל זה לא היה אמור להשפיע על שום דבר חוץ מהגודל שלהם. כדי להשיג זאת, כתבנו מספר גדול של בדיקות יחידות עבור כל איזון. בהתחלה, השוואנו את כל השדות "ראש-על" - בדקנו כל אחד באופן מפורש.זה עבד, אבל זה היה לוקח זמן, ואפילו השינוי הקטן ביותר בשוויון היה לשבור את המבחנים, הכריח אותנו לכתוב אותם מחדש כל הזמן. בסופו של דבר, היה לנו מספיק מזה והגענו לגישה נוחה יותר לביצוע בדיקות להשוואות. השתמשנו בשתי גרסאות של מבני האיזון, כגון SomeBalanceMessage ו- SomeBalanceMessageV2, והשתתפנו בהן – בהשוואה למספר השדות, לשמות ולערכים. תוצאות אופטימיזציה הודות לאופטימיזציות אלה, הצלחנו להפחית את גודל הקבצים המועברים ברשת ואת הזמן הדרוש כדי deserialize אותם על הלקוח. גודל קובץ Old balances Optimized balances Profit Meta balance 9.2 MB 1.28 MB - 86 % Core balance 13.1 MB 2.22 MB - 83 % Meta balance 9.2 MB 1.28 MB 86 אחוז Core balance 13.1 MB 2.2 MB 83 אחוז זמן Deserialization Old balances Optimized balances Profit Meta balance 967 ms 199 ms - 79 % Core balance 1165 ms 265 ms - 77 % Meta balance 967 ש"ח 199 ש"ח 79 אחוז Core balance 1165 ש"ח 265 ש"ח 77 אחוז נתונים בזיכרון Old balances Optimized balances Profit Meta + Core ~ 45.3 MB ~ 33.5 MB - 26 % Meta + Core • 45.3 MB 33,5 MB 26 אחוז מסקנות התוצאות של האופטימיזציה סיפרו לנו שביעות רצון מלאה.הקבצים של איזון היו מופחתים יותר מ -80%.התנועה ירדה, והשחקנים היו מרוצים. לסיכום: היזהרו מהנתונים שאתם מעבירים, ואל תשלחו שום דבר מיותר. החוטים מאוחסנים בצורה הטובה ביותר באחסון ייחודי כדי למנוע יצירת כפולות.אם הנתונים האישיים שלך (מחירים, סטטיסטיקות וכו ') מכילים גם הרבה חזרות, נסה לארוז אותם באחסון ייחודי גם כן.