Cómo Crear un Ranking en Tiempo Real para tu Servidor MU
Aprende a construir un portal de ranking con actualización automática para tu servidor MU Online: base de datos, API backend y frontend en un solo tutorial.
Por Qué un Ranking en Tiempo Real Transforma tu Comunidad
Un servidor de MU Online sin un portal de ranking activo pierde uno de sus principales motores de retención. Los jugadores compiten por posiciones, las guilds se miden entre sí, y los recién llegados usan el ranking para entender qué tan progresado está el servidor antes incluso de conectarse.
El problema con los rankings estáticos actualizados una vez al día es que llegan tarde. Un personaje que acaba de superar a su rival en nivel o resets no lo ve reflejado hasta el día siguiente, y la energía de ese momento competitivo se pierde. Un ranking que se actualiza automáticamente cada 30 a 60 segundos convierte el portal en una extensión del juego en sí mismo.
Este tutorial cubre el recorrido completo: desde entender la estructura de la base de datos del servidor hasta construir el backend y el frontend que mostrarán el ranking actualizado en vivo.
Arquitectura del Sistema: Flujo de Datos
Antes de escribir código, es importante tener clara la separación de responsabilidades. El frontend nunca debe comunicarse directamente con la base de datos del juego. Toda solicitud debe pasar por una capa intermedia que controle qué datos se exponen y cómo.
Base de Datos MU (MySQL / MSSQL)
↓ acceso de solo lectura (SELECT)
API Backend (Node.js / PHP / Python)
↓ endpoint JSON paginado
Capa de Caché (memoria / Redis)
↓ resultado válido por 30–60 s
Frontend (HTML + JavaScript)
↓ polling AJAX cada 45 s
Tabla de Ranking en el Navegador del Jugador
Esta arquitectura en capas ofrece tres ventajas concretas: aísla la base de datos del juego del tráfico web, permite escalar el portal sin afectar al servidor de juego, y centraliza en un solo lugar la lógica de negocio como filtros, paginación y ordenamiento.
> [!ATENCION] > Nunca otorgues permisos de escritura (INSERT, UPDATE, DELETE) al usuario de base de datos que utiliza el portal de ranking. Crea una cuenta dedicada con SELECT únicamente sobre las tablas estrictamente necesarias. Una brecha de seguridad en el portal web no debe poder corromper ni modificar datos reales del servidor de juego.
Tablas y Columnas Relevantes de la Base de Datos MU
La estructura exacta varía según el emulador, pero la mayoría usa convenciones similares. Las columnas más importantes para construir el ranking son las siguientes:
| Columna | Tipo | Descripción |
|---|---|---|
Name | VARCHAR | Nombre del personaje |
Class | TINYINT | Código numérico de la clase actual |
cLevel | SMALLINT | Nivel del personaje (1–400 en Season 6) |
Resets | INT | Número de resets realizados |
GuildName | VARCHAR | Guild a la que pertenece el personaje |
PkCount | INT | Contador de kills de jugadores |
CtlCode | TINYINT | Indicador de cuenta GM o suspendida |
El campo Class almacena un código numérico que representa la clase y la etapa de evolución actual del personaje. La mayoría de emuladores Season 6 utilizan los valores siguientes:
0 → Dark Knight
1 → Blade Knight (primera evolución)
2 → Blade Master (Master Class)
16 → Dark Wizard
17 → Soul Master (primera evolución)
18 → Grand Master (Master Class)
32 → Elf (Fairy Elf)
33 → Muse Elf (primera evolución)
34 → High Elf (Master Class)
48 → Magic Gladiator → Duel Master
64 → Dark Lord → Lord Emperor
80 → Summoner
81 → Bloody Summoner (primera evolución)
82 → Dimension Master (Master Class)
Construyendo el Endpoint de la API Backend
El siguiente ejemplo usa Node.js con la librería mysql2, pero la lógica es equivalente en PHP con PDO o en Python con cualquier conector MySQL. Lo importante es la estructura de la consulta y el formato de respuesta.
// archivo: api/ranking-players.js
// ruta expuesta: GET /api/ranking/players?page=1&limit=50
const mysql = require('mysql2/promise');
const CLASS_NAMES = {
0: 'Dark Knight',
1: 'Blade Knight',
2: 'Blade Master',
16: 'Dark Wizard',
17: 'Soul Master',
18: 'Grand Master',
32: 'Elf',
33: 'Muse Elf',
34: 'High Elf',
48: 'Magic Gladiator → Duel Master',
64: 'Dark Lord → Lord Emperor',
80: 'Summoner',
81: 'Bloody Summoner',
82: 'Dimension Master',
};
async function getRankingPlayers(page = 1, limit = 50) {
const offset = (page - 1) * limit;
// Usa siempre una conexión de solo lectura
const conn = await mysql.createConnection(process.env.DB_READONLY_URL);
const [rows] = await conn.execute(`
SELECT
Name,
Class,
cLevel AS level,
Resets AS resets,
GuildName AS guild
FROM Character
WHERE CtlCode = 0
ORDER BY Resets DESC, cLevel DESC, Name ASC
LIMIT ? OFFSET ?
`, [limit, offset]);
await conn.end();
return rows.map((row, index) => ({
rank: offset + index + 1,
name: row.Name,
class: CLASS_NAMES[row.Class] ?? `Clase desconocida (${row.Class})`,
level: row.level,
resets: row.resets,
guild: row.guild || '—',
}));
}
La consulta ordena primero por Resets de mayor a menor, luego por nivel, y finalmente por nombre alfabético para desempatar. En servidores sin sistema de resets, basta con eliminar la columna Resets del ORDER BY y del SELECT.
Frontend con Actualización Automática
Con el endpoint disponible, el frontend solo necesita consultarlo cada cierto intervalo y repintar la tabla con los datos recibidos. No es necesaria ninguna librería externa: JavaScript nativo con fetch y setInterval es suficiente.
<!-- fragmento del portal de ranking -->
<p id="estado-ranking">Cargando...</p>
<table id="tabla-ranking">
<thead>
<tr>
<th>#</th>
<th>Personaje</th>
<th>Clase</th>
<th>Nivel</th>
<th>Resets</th>
<th>Guild</th>
</tr>
</thead>
<tbody id="cuerpo-ranking"></tbody>
</table>
<script>
const INTERVALO_MS = 45000; // 45 segundos
async function actualizarRanking() {
try {
const respuesta = await fetch('/api/ranking/players?page=1&limit=50');
const jugadores = await respuesta.json();
const cuerpo = document.getElementById('cuerpo-ranking');
cuerpo.innerHTML = jugadores.map(j => `
<tr>
<td>${j.rank}</td>
<td>${j.name}</td>
<td>${j.class}</td>
<td>${j.level}</td>
<td>${j.resets}</td>
<td>${j.guild}</td>
</tr>
`).join('');
document.getElementById('estado-ranking').textContent =
'Última actualización: ' + new Date().toLocaleTimeString('es-ES');
} catch (error) {
document.getElementById('estado-ranking').textContent =
'Error al cargar el ranking. Reintentando...';
}
}
// Primera carga inmediata y luego en intervalos regulares
actualizarRanking();
setInterval(actualizarRanking, INTERVALO_MS);
</script>
El bloque try/catch alrededor del fetch es importante: si el servidor web o la base de datos tienen un pico de carga temporal, el frontend mostrará un mensaje informativo en lugar de romperse visualmente.
> [!CONSEJO] > Para mejorar la experiencia visual, guarda el array de jugadores anterior en una variable antes de reemplazarlo con el nuevo. Compara posición por nombre entre los dos snapshots y agrega una clase CSS como subio o bajo a cada fila. Un triángulo verde apuntando hacia arriba o rojo hacia abajo junto al número de posición hace el ranking mucho más atractivo y mantiene a los jugadores revisando el portal con más frecuencia.
Capa de Caché para Proteger la Base de Datos
Cuando el servidor tiene cientos de jugadores activos, varios de ellos pueden estar consultando el ranking al mismo tiempo. Sin caché, cada solicitud al portal genera una consulta completa a la base de datos del juego, compitiendo por recursos con el propio servidor de MU.
La solución más simple es guardar el resultado en memoria durante el intervalo de actualización:
Solicitud entrante al endpoint
↓
¿Existe caché válido? → Sí → devuelve JSON en memoria (sin tocar BD)
↓ No
Ejecuta SELECT en la base de datos
↓
Almacena resultado en caché con timestamp
↓
Devuelve JSON al cliente
En Node.js esto se puede implementar con un simple objeto y una comparación de timestamps. En entornos con más tráfico, Redis ofrece la misma lógica con persistencia entre reinicios del proceso y compatibilidad con múltiples instancias del servidor web.
Un TTL (tiempo de vida del caché) de 30 a 60 segundos es suficiente. Los jugadores no esperan que el ranking sea preciso al segundo, y ese margen elimina decenas o cientos de consultas por minuto durante las horas pico.
Rankings Adicionales: Guilds y PK
El mismo patrón de endpoint + caché + frontend con polling funciona para otros tipos de ranking que enriquecen el portal:
Ranking de Guilds: agrupa por GuildName y suma los resets o niveles de todos los miembros. Útil para comunidades orientadas al juego grupal y eventos como Castle Siege.
Ranking de PK: ordena por PkCount DESC y muestra el estado actual del personaje. Es uno de los rankings más revisados en servidores con sistema de PvP libre, ya que crea dinámicas sociales de cazador y cazado dentro de la comunidad.
Ambos rankings se exponen como endpoints separados en la misma API y comparten la misma capa de caché, simplemente con claves distintas para cada tabla de resultados.
Verificación Final Antes de Publicar
Antes de hacer el portal accesible al público, revisa los siguientes puntos:
- El usuario de base de datos tiene solo permisos SELECT sobre las tablas necesarias.
- El endpoint de la API no expone datos sensibles como contraseñas, correos electrónicos ni IPs de jugadores.
- El filtro
WHERE CtlCode = 0excluye correctamente a los personajes GM y suspendidos. - La paginación con LIMIT y OFFSET está activa para evitar devolver miles de registros en una sola respuesta.
- El intervalo de polling en el frontend no es menor a 15 segundos incluso con caché activo.
Un portal de ranking bien construido refuerza la sensación de competencia activa dentro del servidor y es una de las herramientas más efectivas para que los jugadores regresen a revisar su posición día tras día.
Perguntas frequentes
¿Con qué frecuencia debo actualizar el ranking?
Un intervalo de 30 a 60 segundos mediante polling AJAX es suficiente para la mayoría de los servidores. Consultar la base de datos con más frecuencia aumenta la carga sin ofrecer una mejora perceptible para el jugador. Si utilizas una capa de caché en memoria o Redis, puedes reducir el intervalo a 15 segundos sin afectar el rendimiento del servidor de juego.
¿Qué usuario de base de datos debo usar para el portal?
Crea un usuario exclusivo con permiso SELECT únicamente sobre las tablas necesarias (Character, Guild, etc.). Nunca uses el usuario administrador del servidor MU ni una cuenta con permisos de escritura. Una cuenta de solo lectura limita el daño en caso de que el portal sea comprometido.
¿Puedo mostrar el ranking de guilds además del individual?
Sí. Solo necesitas agrupar por el campo GuildName y agregar los valores de nivel o resets de cada miembro. Puedes exponer este cálculo como un segundo endpoint en tu API, por ejemplo /api/ranking/guilds, y reutilizar el mismo frontend con una tabla diferente.
¿Qué hago si el ranking carga lento cuando hay muchos jugadores conectados?
El cuello de botella casi siempre está en la consulta SQL sin índices adecuados. Asegúrate de tener un índice compuesto sobre las columnas usadas en el ORDER BY (Resets, cLevel). Añade además una capa de caché simple en memoria que almacene el resultado durante 30-60 segundos y lo sirva directamente sin tocar la base de datos en cada solicitud.