Mi nombre es Sergey Kachan, y soy un desarrollador de cliente en el proyecto War Robots. War Robots ha estado ahí durante muchos años, y durante este tiempo el juego ha acumulado una gran variedad de contenidos: robots, armas, drones, titanes, pilotos, y así sucesivamente. Hoy voy a hablar de cómo se estructuran los equilibrios en nuestro proyecto, qué les ha pasado en los últimos 11 años, y cómo hemos tratado con ello. Balances en el proyecto Como cualquier otro proyecto, War Robots se puede dividir en dos partes: meta y juego principal. Es cualquier actividad que va más allá del núcleo del juego, pero todavía afecta al juego. Esto incluye la compra y la actualización de contenido del juego, la participación en actividades sociales o eventos. Meta gameplay (metagaming) es el principal ciclo repetitivo de acciones que el jugador realiza en el juego para alcanzar sus objetivos. En nuestro caso, son batallas de robots en mapas específicos. Core gameplay (core gameplay loop) Cada parte del proyecto necesita su propio equilibrio, por lo que también dividimos los equilibrios en dos categorías: meta y núcleo. Los robots de guerra también tienen los llamados que requieren sus propios equilibrios separados. Skirmish modes a Es una modificación de los modos o mapas existentes con características o reglas diferentes.Los modos de Skirmish a menudo se basan en eventos, disponibles para los jugadores durante varias vacaciones, principalmente para divertirse. Skirmish mode Así que en total, tenemos 4 saldos: 2 para el modo predeterminado y 2 para el modo Skirmish. Durante 11 años, War Robots ha acumulado una tonelada de contenido increíble: 95 robots 21 Titán 175 armas diferentes Cuatro drones 16 Maternidad un gran número de skins, remodelaciones, módulos, pilotos, torres, versiones finales de contenido y mapas Y como se puede imaginar, para hacer todo este trabajo necesitamos almacenar información sobre el comportamiento, estadísticas, disponibilidad, precios y mucho, mucho más. Como resultado, nuestros saldos han crecido a un tamaño indecente: 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 Después de algunos cálculos rápidos, encontramos que un jugador necesitaría descargar ¡Eso es bastante mucho! 44.6 MB Realmente no queríamos forzar a los jugadores a descargar tan grandes cantidades de datos cada vez que cambiaba el balance. Sólo para recordarles: Los robots de guerra han llegado En 2024, nuestra audiencia activa mensual fue , y registrado en cada día. 300 million registered users 4.7 million people 690 thousand players Ahora imagina la cantidad de datos. Mucho, ¿verdad? también lo pensamos. ¡Así que decidimos hacer todo lo que podíamos para reducir el tamaño de nuestros saldos! Cazando el problema El primer paso fue analizar los equilibrios y tratar de averiguar: “¿Qué está ocupando tanto espacio?” Pasar por todo manualmente fue lo último que queríamos hacer: habría tomado años, así que escribimos un conjunto de herramientas que recogieron y agregaron toda la información que necesitábamos sobre los saldos. La herramienta tomaría un archivo de balance como entrada y, utilizando la reflexión, iteraría a través de todas las estructuras, recogiendo datos sobre qué tipos de información almacenamos y cuánto espacio ocupaba cada una. Los resultados fueron desalentadores: Meta de equilibrio % 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 por ciento 161 312 Boolean 6.329 por ciento 36 568 Double 5.845 por ciento 33 772 Int64 4.682 por ciento 27 054 personas Custom structures El 26,749 % — Equilibrio básico % 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 por ciento 232 229 Double 23.370 por ciento 158 418 Int32 El 20 955 % 142 050 Boolean 5.306 por ciento 34 323 Custom structures 16,11 por ciento — Después de analizar la situación, nos damos cuenta de que Y algo tenía que hacerse al respecto. strings were taking up far too much space Esta escaneó el archivo de balance y generó un mapa de todas las cadenas junto con el número de veces que cada una se duplicó. Los resultados tampoco fueron alentadores.Algunas cadenas se repitieron decenas de miles de veces! Habíamos encontrado el problema.Ahora la pregunta era: ¿cómo lo arreglaremos? Optimización de los equilibrios Por razones obvias, no podíamos simplemente deshacernos de las cadenas por completo. Las cadenas se utilizan para cosas como claves de localización y varios IDs. La idea es tan simple como se consigue: Crear una lista de cadenas únicas para cada balance (en esencia, un almacenamiento dedicado). Envía esta lista junto con los datos. public class BalanceMessage { public BalanceMessageData Data; public StringStorage Storage; public string Version; } StringStorage es esencialmente un envoltorio alrededor de una lista de cadenas. Cuando construimos el almacenamiento de cadenas, cada estructura de equilibrio recuerda el índice de la cadena que necesita. Más tarde, cuando recuperamos datos, simplemente pasamos el índice y rápidamente obtenemos el valor. public class StringStorage { public List<string> Values; public string GetValue(StringIdx id) => Values[id]; } En lugar de pasar las cadenas dentro de las estructuras de equilibrio, comenzamos a pasar el índice de dónde se almacena la cadena en el almacenamiento de cadenas. Antes de: public class SomeBalanceMessage { public string Id; public string Name; public int Amount; } Después de: public class SomeBalanceMessageV2 { public StringIdx Id; public StringIdx Name; public int Amount; } StringIdx es básicamente sólo un envoltorio alrededor de un int. De esta manera, eliminamos completamente las transferencias directas de cuerdas dentro de las estructuras de equilibrio. 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; } Este enfoque redujo el número de cuerdas en decenas de veces. 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 personas Core balance 232 229 14 228 No es malo, ¿verdad? Pero eso fue sólo el comienzo – no nos detuvimos allí. Reestructuración del protocolo de datos Para la transmisión y el procesamiento de estructuras de equilibrio, habíamos estado utilizando . MessagePack MessagePack es un formato de serialización de datos binario diseñado como una alternativa más compacta y más rápida a JSON. Está diseñado para el intercambio de datos eficiente entre aplicaciones o servicios, permitiendo una reducción significativa en el tamaño de los datos - especialmente útil cuando el rendimiento y el ancho de banda son importantes. Inicialmente, MessagePack apareció en un formato similar a JSON, donde los datos utilizados Eso es particularmente conveniente, pero también bastante espacioso. por lo que decidimos sacrificar algo de flexibilidad y cambiar a una . string keys binary byte array Antes de: public class SomeBalanceMessage { [Key("id")] public string Id; [Key("name")] public string Name; [Key("amount")] public int Amount; } Después de: public class SomeBalanceMessageV2 { [Key(0)] public StringIdx Id; [Key(1)] public StringIdx Name; [Key(2)] public int Amount; } También eliminamos todas las colecciones vacías – en lugar de enviarlas, ahora transmitimos valores nulos. Esto redujo tanto el tamaño general de los datos como el tiempo necesario para la serialización y la deserialización. Probar los cambios Una regla de oro de buen desarrollo (y una que te ahorrará muchos nervios) es implementar siempre nuevas características de una manera que te permita volverlas rápidamente si algo va mal. Por esa razón, añadimos todas las nuevas características detrás de “trucos”. Durante el desarrollo, necesitábamos asegurarnos de que todos los datos se transferían correctamente.Los equilibrios antiguos y nuevos, independientemente del formato o de la estructura, debían producir exactamente los mismos valores.Y recuerda, los equilibrios optimizados habían cambiado drásticamente su estructura, pero eso no debía afectar a nada más que a su tamaño. Para lograr esto, escribimos un gran número de pruebas de unidades para cada balance. Al principio, comparamos todos los campos "en la cabeza" - comprobando cada uno explícitamente. Esto funcionó, pero fue demorado, y incluso el menor cambio en los equilibrios rompió las pruebas, obligándonos a reescribirlos constantemente. Al final, nos aburrimos de eso y llegamos a un enfoque de prueba más conveniente para comparar los equilibrios. Hemos tomado dos versiones de las estructuras de equilibrio, por ejemplo, SomeBalanceMessage y SomeBalanceMessageV2, y hemos iterado sobre ellas, comparando números de campos, nombres y valores. Resultados de optimización Gracias a estas optimizaciones, conseguimos reducir tanto el tamaño de los archivos transmitidos a través de la red como el tiempo necesario para deserializarlos en el cliente. Tamaño de archivo 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 El 86% Core balance 13.1 MB 2.22 MB - El 83 % Tiempo de desertificación Old balances Optimized balances Profit Meta balance 967 ms 199 ms - 79 % Core balance 1165 ms 265 ms - 77 % Meta balance 967 ms 199 ms - El 79 % Core balance 1165 ms 265 ms - El 77% Datos en memoria Old balances Optimized balances Profit Meta + Core ~ 45.3 MB ~ 33.5 MB - 26 % Meta + Core - 54,3 MB · 33,5 MB El 26% Conclusiones Los resultados de la optimización nos satisfieron plenamente.Los archivos de balance se redujeron en más de 80%.El tráfico se redujo, y los jugadores estaban contentos. Para resumir: tenga cuidado con los datos que transmita, y no envíe nada innecesario. Las cadenas se almacenan mejor en almacenamientos únicos para evitar crear duplicados.Y si sus datos personalizados (precios, estadísticas, etc.) también contienen mucha repetición, intente empacarlos en almacenamientos únicos también.