1 puntos por GN⁺ 2024-11-24 | Aún no hay comentarios. | Compartir por WhatsApp
  • En un entorno FPS rápido, la información de estado que llega tarde tiene poco valor, por lo que Quake 3 optó por un diseño centrado en UDP/IP para reducir la latencia
  • NetChannel abstrae la comunicación sobre UDP, que puede tener pérdidas, y el servidor recalcula solo las diferencias de estado necesarias mediante un historial de snapshots por cliente
  • El servidor usa en conjunto el Master Gamestate, los 32 gamestates recientes y un dummy gamestate para convertir las actualizaciones completas y delta en el mismo procedimiento
  • Si no hay ACK del cliente, el servidor compara el último snapshot confirmado con el estado actual y coloca en un solo mensaje los cambios omitidos y los cambios nuevos
  • Aunque C no tiene introspección integrada, se encuentran diferencias de campos con netField_t y macros, y NetChannel evita la fragmentación en routers con una división previa en 1400 bytes

Modelo de red basado en UDP/IP

  • El modelo de red de Quake 3 se considera una de las partes más elegantes del motor y, en el nivel más bajo, abstrae la comunicación con el módulo NetChannel, que apareció por primera vez en Quake World
  • En un juego rápido, la información que se pierde en el primer envío pronto se vuelve información obsoleta, por lo que conviene más enviar el estado más reciente que reenviarla
  • Por eso, en el motor no hay rastros de TCP/IP, ya que se consideró difícil aceptar la latencia que genera la transmisión confiable
  • En la pila de red se agregan dos capas mutuamente excluyentes
    • Cifrado mediante una clave precompartida
    • Compresión mediante una clave Huffman precalculada
  • El servidor reduce el tamaño de los datagramas UDP y, al mismo tiempo, compensa la falta de confiabilidad
    • Genera paquetes delta con el historial de snapshots
    • Busca y envía solo los campos modificados mediante un mecanismo de introspección de memoria

Roles del servidor y del cliente

  • El flujo del lado del cliente es simple
    • Envía comandos al servidor en cada frame
    • Recibe actualizaciones de gamestate desde el servidor
  • El servidor debe propagar el Master Gamestate a cada cliente, tomando en cuenta incluso los paquetes UDP perdidos
  • El mecanismo central se compone de tres elementos
    • Master Gamestate: el estado del juego universalmente verdadero; los comandos del cliente entran por NetChannel, se convierten en event_t y luego modifican el estado del juego en el servidor
    • Los 32 gamestates recientes por cliente: los estados enviados por la red se guardan en un arreglo circular y se llaman snapshots
    • dummy gamestate: un estado en el que todos los campos son 0 y se usa como referencia para generar deltas cuando no hay estado anterior
  • Con estos tres elementos, el servidor crea el mensaje de actualización que entregará a NetChannel
  • Como hay que mantener muchos gamestates por cliente, el uso de memoria aumenta
    • Según la medición, usa 8 MB para 4 jugadores

Crear actualizaciones completas y parciales con snapshots

  • El ejemplo usa una situación en la que se envía una actualización a Client1, y el estado de Client2 está compuesto por cuatro campos: pos[X], pos[Y], pos[Z] y health
  • La comunicación se realiza por UDP/IP, y en Internet los mensajes pueden perderse con frecuencia
  • Primer frame del servidor

    • El servidor aplica al Master Gamestate todas las actualizaciones recibidas de los clientes y luego propaga el estado a Client1
    • El módulo de red sigue siempre el mismo procedimiento
    • Copia el Master Gamestate en el siguiente slot del historial del cliente
    • Compara el snapshot copiado con otro snapshot
    • En la primera actualización no hay un snapshot válido en el historial de Client1, por lo que lo compara con el dummy snapshot
    • Como todos los campos del dummy snapshot son 0, el resultado es una actualización completa
    • Antes de cada campo se agrega un marcador de bit que indica si cambió o no
    • La actualización completa de ejemplo usa 132 bits
    • El formato es [1 A_on32bits 1 B_on32bits 1 B_on32bits 1 C_on32bits]
  • Segundo frame del servidor

    • En el siguiente frame, Client2 se mueve sobre el eje Y y el valor de pos[1] pasa a ser E
    • Client1 hizo ACK de la actualización anterior, por lo que Snapshot1 queda en estado ACK
    • El servidor copia el Master Gamestate en el siguiente slot del historial para crear Snapshot2 y lo compara con el Snapshot1 válido
    • Como resultado, solo se transmite por la red el cambio pos[1] = E
    • Como cada campo lleva un marcador de bit, esta actualización parcial usa 36 bits
    • El formato es [0 1 32bitsNewValue 0 0]
  • Tercer frame del servidor

    • En el siguiente frame, Client2 pierde vida y pasa a health = H
    • Client1 no hace ACK de la última actualización
    • Puede que se haya perdido el paquete UDP del servidor, o que se haya perdido el ACK del cliente
    • En cualquiera de los casos, ese snapshot no se puede usar
    • El servidor copia el Master Gamestate en el siguiente slot para crear Snapshot3 y lo compara con el último Snapshot1 confirmado con ACK
    • El mensaje transmitido es una actualización parcial e incluye tanto el cambio anterior pos[1] = E como el nuevo cambio health = H
    • Si Snapshot1 es demasiado antiguo y no se puede usar, el motor vuelve a enviar una actualización completa tomando como referencia el dummy snapshot

Cómo compensa las pérdidas con el mismo procedimiento

  • La simplicidad del sistema de snapshots está en que el mismo algoritmo procesa automáticamente dos tareas
    • Generar una actualización completa o parcial
    • Reenviar en un solo mensaje la información anterior no recibida y la información nueva
  • La pérdida de paquetes UDP no se maneja con un flujo complejo aparte; se compensa calculando la diferencia entre el último snapshot confirmado con ACK y el Master Gamestate actual
  • Cuando no hay estado anterior o no se puede usar, se envía el estado completo tomando como referencia el dummy snapshot para recuperarse

Cómo encontrar diferencias de campos en C

  • Quake 3 no tiene introspección en el lenguaje C, pero la posición de cada campo se configura de antemano con un arreglo netField_t y directivas de preprocesador
  • netField_t contiene el nombre del campo, el offset y la cantidad de bits
  • La macro NETF(x) permite escribir la información del campo de forma breve usando el operador de conversión a cadena y el cálculo del offset sobre entityState_t
  • La estructura de ejemplo es la siguiente
typedef struct { char *name; int offset; int bits; } netField_t;

// using the stringizing operator to save typing...
#define NETF(x) #x,(int)&((entityState_t*)0)->x

netField_t entityStateFields[] = {
    { NETF(pos.trTime), 32 },
    { NETF(pos.trBase[0]), 0 },
    { NETF(pos.trBase[1]), 0 },
    ...
}
  • La implementación completa está en parte de MSG_WriteDeltaEntity
  • Quake 3 no interpreta el significado de lo que compara; sigue el índice, offset y tamaño de entityStateFields, y transmite por la red las diferencias

Por qué dividir previamente en 1400 bytes

  • El módulo NetChannel divide los mensajes en fragmentos de 1400 bytes, aunque el tamaño máximo de un datagrama UDP sea de 65507 bytes
  • El código relacionado está en Netchan_Transmit
  • Como la mayoría de las MTU de red son de 1500 bytes, dividir en 1400 bytes fue una decisión para evitar que los routers fragmenten paquetes en la ruta de Internet
  • Hay dos razones para evitar la fragmentación en routers
    • Al entrar a la red, el router debe retener el paquete mientras lo fragmenta
    • Al salir de la red, debe esperar todos los fragmentos del datagrama y luego hacer un reensamblado costoso

Mensajes que deben entregarse obligatoriamente

  • El sistema de snapshots compensa los datagramas UDP perdidos en la red, pero algunos mensajes y comandos deben entregarse obligatoriamente
  • Esto incluye casos como cuando un jugador sale o cuando el servidor exige al cliente cargar un nuevo nivel
  • Esta garantía la abstrae NetChannel

Lecturas relacionadas

Aún no hay comentarios.

Aún no hay comentarios.