Warcraft Rumble

Warcraft Rumble entre bastidores: calcular la experiencia de una mini

Blizzard Entertainment

¡Saludos, compi de Rumble!

Soy Andy Lim, ingeniero jefe de servidores de Warcraft Rumble. El equipo de servidores es responsable de una gran variedad de tareas, como la gestión de redes, la computación en la nube o el almacenamiento, pero también creamos características del juego como la progresión de la campaña y las misiones. Hoy me gustaría hablarte un poco más sobre cómo almacenamos la experiencia y la utilizamos para calcular los niveles de cada mini.


Cassandra

¡Atención! Vamos a entrar en detalles técnicos y la cosa podría ponerse un poco pesada, pero no te vayas todavía.

Empecemos conociendo un poco la solución que hemos ideado en cuanto al almacenamiento para seguir de cerca los frecuentes cambios en los datos de nuestros jugadores para tantos usuarios simultáneos: Cassandra. Cassandra es una base de datos de código abierto muy popular, sumamente escalable y distribuida, que nos proporciona el equilibrio adecuado entre consistencia y disponibilidad de datos. En cuanto a los datos, Cassandra se maneja bien con enormes conjuntos de datos sin forzar un esquema rígido. Hemos creado herramientas que permiten a nuestros ingenieros definir nuestras tablas de bases de datos y de esquemas según las necesidades de cada característica, lo que nos da cierta flexibilidad a la hora de estructurar y organizar. De esta manera, podemos escribir y validar con facilidad nuestro esquema y nuestras consultas.

¿Y qué es un esquema? Un esquema de base de datos define cómo se organizan los datos dentro de una base de datos relacional. Es como lo que puedes encontrar una tabla, por ejemplo.


Almacenar la experiencia como un libro de contabilidad

Una peculiaridad de Cassandra es que, aunque muy rápida a la hora de escribir datos, es muy lenta para leerlos. La actualización de datos in situ suele ser una operación de lectura y luego escritura, por lo que también sería lenta. Para evitarlo, hemos diseñado los datos de nuestros jugadores para que se almacenen como si se tratara de un libro de contabilidad: escribimos cada línea como un cambio, y cuando se lee, se leen todas las entradas para luego hacer algunos cálculos.

Un buen ejemplo de esto es lo que ocurre con tu tarjeta de crédito. Cada transacción se escribe como una única entrada con valor positivo o negativo. Cada vez que la usas, hay una transacción con valor negativo; y cada vez que pagas la deuda, la transacción tiene valor positivo. ¿Cómo sabe cuánto debe? Pues mirando toda la contabilidad y sumando o restando cada valor.

Piensa en los datos in situ como si tuvieras una única cosa que puedes almacenar. Cada vez que quieras cambiar ese valor, debes leerlo, hacer el cambio y luego escribir cuál es el nuevo valor. Por ejemplo, si se trata de un resultado de un partido de fútbol, cada vez que un equipo marca un gol, debes hacer una lectura para consultar el último resultado, añadir el nuevo gol, y luego escribir el nuevo resultado.

Un ejemplo concreto para Archlight es el libro de experiencia de cada mini. Después de cada misión, se añade una nueva fila al libro para cada mini, indicando la cantidad de experiencia que ha ganado. Tras cinco misiones, las entradas podrían ser como estas:

Tabla 1

Jugador

Mini

Valor

Fecha

Andrés

Bruto gnoll

3

Lunes, 14:00

Andrés

Jinete de grifos

3

Lunes, 14:05

Andrés

Bruto gnoll

3

Lunes, 14:10

Andrés

Piloto de SEGURO

3

Lunes, 14:15

Andrés

Bruto gnoll

3

Lunes, 14:20

Por último, para obtener la experiencia total de una mini, simplemente recuperamos todas esas entradas, las agrupamos para cada mini y sumamos los valores de experiencia. El resultado sería el siguiente:

Tabla 2

Jugador

Mini

Total

Andrés

Bruto gnoll

9

Andrés

Jinete de grifos

3

Andrés

Piloto de SEGURO

3


Cálculos parciales

Ahora que ya sabemos cómo almacenamos la experiencia, veamos cómo podemos refinar los cálculos que hay que hacer para cada mini. Almacenar filas de datos individuales para cada mini que gane experiencia crearía una tabla sin fin y con una cantidad de datos inconmensurable... PARA SIEMPRE. Digamos que un día típico en Rumble es una combinación de 15 misiones o JcJ, junto a las 20 oleadas y la mazmorra semanal para mejorar tus minis. Un total de 100 entradas semanales en la tabla. Después de unos 3 meses, la tabla contaría con unas 1260 entradas. Ahora, para calcular los totales, Cassandra tendría que leer lentamente cada entrada. Y no queremos eso.

Hemos diseñado una solución para ello: los cálculos parciales, un cálculo de valores hasta un punto específico. En este caso, los sumaremos todos y los representaremos en una única fila que almacenaremos en una segunda tabla. Ahora puedes ver lo útil que es la marca de tiempo. Para la experiencia, la clave será el jugador y la mini, y el cálculo una suma. Hagamos un cálculo parcial de todas las entradas hasta el martes a las 12:00 y almacenemos los resultados en una segunda tabla Cassandra:

Tabla 3

Jugador

Mini

Total

Fecha final

Andrés

Bruto gnoll

9

Martes, 12:00

Andrés

Jinete de grifos

3

Martes, 12:00

Andrés

Piloto de SEGURO

3

Martes, 12:00

Las partidas que juegues el miércoles generan más entradas de experiencia, por lo que la tabla de experiencia original crecería.

Tabla 4

Jugador

Mini

Valor

Fecha

Andrés

Bruto gnoll

3

Lunes, 14:00

Andrés

Jinete de grifos

3

Lunes, 14:05

Andrés

Bruto gnoll

3

Lunes, 14:10

Andrés

Piloto de SEGURO

3

Lunes, 14:15

Andrés

Bruto gnoll

3

Lunes, 14:20

Andrés

Piloto de SEGURO

3

Miércoles, 12:00

Andrés

Cadena de relámpagos

3

Miércoles, 12:05

Andrés

Jinete de grifos

3

Miércoles, 12:10

Ahora queremos hacer una búsqueda completa y volver a calcular cuáles son los niveles de las minis después de jugar esas partidas. Consultamos ambas tablas, esto es, la tabla de cálculo parcial para la entrada única y la general para las entradas posteriores, y sumamos los valores para obtener una tabla final con el total de experiencia. Por lo tanto, leemos la tabla 3 y luego consultamos la tabla 4 solo para los datos que existen después de cada fila de la tabla 3 siguiendo estos pasos:

  1. Leemos una fila de la tabla 3
     

Jinete de grifos

3

Martes, 12:00

  1. Leemos las filas del jinete de grifos de la tabla 3 después del martes a las 12:00
     

Jinete de grifos

3

Miércoles, 12:10

  1. Ahora sumamos ambos datos para generar un resultado total:

Jinete de grifos

6

Ya te darás cuenta de que este proceso cortocircuitaría bastantes de las lecturas de la tabla 4

Puede que pienses que deberíamos limitarnos a almacenar los datos recién calculados después de registrar una nueva entrada en la tabla. Esto puede parecer una buena idea, pero daría lugar a incoherencias en los datos y a una degradación del rendimiento. Por ejemplo, el sistema estaría ocupado recalculando y almacenando datos, a menudo debido a la necesidad de leer estos datos y luego escribirlos. Debemos equilibrar el cálculo mientras el juego se ejecuta con un rendimiento adecuado para todos los jugadores.

Uno de los problemas derivados de la inconsistencia de datos se daría a la hora de realizar los cálculos al final de la partida. Nuestra infraestructura de servidores y plataformas admite los mensajes que llegan tarde. Un ejemplo sería un problema transitorio entre dos centros de datos (A y B), ya que el mensaje para otorgarle experiencia a la mini de un jugador se envía desde A, pero aún no habría llegado a B. Imagínate esta situación: estás jugando una partida justo antes de medianoche. Juegas y ganas, y es miércoles a las 23:59. El procesador de cálculo parcial está configurado para ejecutarse diariamente y para todos los datos hasta la jornada presente. Dicho esto, arrancaría a medianoche y calcularía los valores totales de la experiencia obtenida hasta el jueves a las 00:00. Esos datos se almacenarían en la segunda tabla, y las nuevas lecturas de la experiencia buscarían el último valor almacenado para sumarlo todo pasado el jueves a las 00:00. Lo que sucede si un mensaje llega con retraso es que sí, lo recibimos, pero ten en cuenta que la partida se completó el miércoles a las 23:59 y se escribió el registro de experiencia a esa hora. La tabla de cálculo parcial no tendría en cuenta esta entrada y una petición posterior de los datos tampoco mostraría este registro. Por eso, estos datos incompletos supondrían un problema. ¿Y qué hay de los mensajes que llegan con unos días de retraso? Bueno, pues disponemos de sistemas de seguimiento y de alertas que casi siempre garantizan que la información nueva se procesará a tiempo para el siguiente cálculo parcial.


Calcular el nivel de las minis

Si nos fijamos en la tabla de experiencia total, verás que no se reflejan los niveles calculados. Solo disponemos del total de experiencia obtenida. Existe una tabla estática por separado que han creado los diseñadores de juego que tiene este aspecto. Una mini comienza en el nivel 1 y, en cuanto obtenga 1 punto de experiencia, subirá al nivel 2. Después, deberá obtener 3 puntos de experiencia para subir al nivel 3.

Nivel

Cantidad hasta el siguiente nivel

1

1

2

3

3

6

4

10

5

20

...

...

10

250

Si combinamos ambas tablas podemos obtener en tiempo real el nivel de una mini si calculamos el total, lo que nos facilita este último conjunto de datos:

Mini

Nivel

Experiencia

Cantidad hasta el siguiente nivel

Bruto gnoll

3

5

1

Jinete de grifos

3

2

4

Piloto de SEGURO

3

2

4

Cadena de relámpagos

2

2

1

Los diseñadores de juego disponen de la flexibilidad para modificar la experiencia necesaria para subir de nivel. Puede que lleguen a la conclusión de que es muy complicado subir de nivel en los niveles más bajos y reduzcan la cantidad necesaria para hacerlo. Esto implicaría que, sin realizar cambios a los datos guardados de los jugadores en la base de datos, las minis recibirían un empujoncito a su nivel actual y que necesitarían menos experiencia para subir al siguiente. Esto también ocurre al contrario. Si los diseñadores de juego deciden que se sube demasiado rápido y aumentan los valores, no otorgaremos experiencia gratuita para que las minis tengan el nivel previo al cambio. Eso no quiere decir que no podamos darles a los de diseño la opción de otorgar un poco de experiencia a las minis como medio para solucionar este problema.


Alternativas ponderadas

Antes de usar la solución que ya tenemos, estuvimos estudiando varias alternativas sobre cómo almacenar la obtención de experiencia y cómo calcular el nivel resultante para las minis. Algunas opciones provocaban mantenimientos adicionales que nos permitían modificar la experiencia el jugador, mientras que otros rendían bien a la hora de ofrecer la experiencia adecuada mientras seguían el ritmo de la progresión del jugador.

Almacenar el nivel y la experiencia de una mini hasta el siguiente nivel

¿Y si usáramos un patrón SQL estándar con actualizaciones transaccionales? Este tipo de actualizaciones permiten actualizar un conjunto de datos al completo (o todos o ninguno). Si una parte da error y no se escribe, todo vuelve a su valor original. Este método otorga atomicidad, regularidad, aislamiento y durabilidad.

Además, se usaría una única columna para mantener la experiencia y el nivel total de una mini, lo que provocaría que las lecturas fuesen muy ligeras.

A medida que una mini obtiene experiencia, hay que actualizarla para que ya tenga 2 PE y le falten otros 8 PE para llegar al nivel 3. Este tipo de cambio obliga a una lectura de los datos, a recalcularlos y a escribirlos en la base de datos. Este patrón no combina del todo bien con Cassandra y sus ventajas. Otro problema con este enfoque sería que para cambiar la cantidad necesaria para subir de nivel haría falta desconectar el juego para que podamos modificar los datos de las minis de todos los jugadores.

Almacenarlo todo en forma porcentaje

También pensamos en usar un porcentaje hasta el siguiente nivel. Por ejemplo:

Mini

Nivel

Hasta el siguiente nivel

Bruto gnoll

3

20 %

Jinete de grifos

2

20 %

Piloto de SEGURO

2

20 %

Entonces, tras una partida, realizaríamos una serie de cálculos rápidos. Por ejemplo, si el bruto gnoll obtiene +3 PE, el porcentaje será +3/10. Le otorgaremos al bruto gnoll un 30 % del nivel, lo que genera lo siguiente:

Mini

Nivel

Hasta el siguiente nivel

Bruto gnoll

3

50 %

Jinete de grifos

2

20 %

Piloto de SEGURO

2

20 %

Este método nos dará flexibilidad si cambiamos la curva de nivel de las minis, pero no habría cambios de nivel en la propia mini. Además, nos facilitaría una visualización más sencilla en la interfaz de usuario del juego porque, por ejemplo, nos limitaríamos a decir que falta rellenar un 30 % de la barra sin cálculos adicionales. No obstante, este patrón desembocaría en unos porcentajes muy reducidos en los niveles más altos. Si una mini recibe 10 PE por partida y se necesitan 100 000 PE para subir de nivel, ese valor se representaría a un 0,01 %. Las unidades centrales de procesamiento (CPU) pueden gestionar decimales, pero la precisión y la exactitud pueden generar errores si no tenemos en cuenta la naturaleza de los cálculos con este tipo de cifras.


Hasta la próxima...

Barajamos muchas opciones en cuanto a la tecnología de la base de datos, las herramientas que podíamos utilizar y, por último, el almacenamiento y el cálculo de todo ello. Como cualquier cosa que esté en desarrollo, existe la posibilidad de que cambiemos todo esto más tarde si encontramos algo que funcione aún mejor. No obstante, lo que tenemos parece un buen punto de partida para este juego.

¡Gracias por acompañarnos en esta actualización de ingeniería!

~ Andy Lim

Por cierto, confirmo de forma oficial que no soy el bandido de los ojos saltones. Durante mi primer día en este equipo, el bandido llenó de pegatinas de ojos saltones mis monitores nuevos. Me miran... todo el día.