El mayor portal de MU Online de Brasil — desde 2003
Tutorial Intermedio Servidor

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.

EQ Equipo ViciadosMU · Actualizado el 4 jul 2026 · ⏱ 18 min de lectura

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:

ColumnaTipoDescripción
NameVARCHARNombre del personaje
ClassTINYINTCódigo numérico de la clase actual
cLevelSMALLINTNivel del personaje (1–400 en Season 6)
ResetsINTNúmero de resets realizados
GuildNameVARCHARGuild a la que pertenece el personaje
PkCountINTContador de kills de jugadores
CtlCodeTINYINTIndicador 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)
Nota: El campo CtlCode vale 0 para cuentas normales de jugador. Los personajes GM, cuentas de prueba y cuentas suspendidas tienen valores distintos. Siempre filtra con WHERE CtlCode = 0 para excluirlos del ranking público, ya que sus estadísticas suelen estar infladas y distorsionarían la tabla.

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 = 0 excluye 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.

EQ

Equipo ViciadosMU

Equipe editorial do ViciadosMU — portal de MU Online no ar desde 2003.

Sigue leyendo

Artículos relacionados