Cuando decidi reescribir el multijugador de Warman desde cero, opte por simulacion lockstep. La idea es simple: no sincronices el estado del juego, sincroniza los inputs. Cada cliente ejecuta exactamente la misma simulacion. Mientras los inputs sean identicos y la simulacion sea determinista, el estado del juego permanece sincronizado sin enviarlo nunca por la red.
Fue una decision practica. Warman puede tener cientos de enemigos, proyectiles, efectos de suelo y modificadores activos al mismo tiempo. Sincronizar todo ese estado cada frame seria costoso. Con lockstep, el coste de red es constante independientemente de la complejidad del juego. Da igual si hay 10 enemigos o 500, se envian los mismos paquetes pequenos de input.
El Comando de Input
Cada tick, cada jugador produce un PlayerInputCommand. Esto contiene: un vector de movimiento (cuantizado a un byte por eje. El rango float de -1 a 1 se mapea a 0-255), una posicion de cursor relativa al jugador (para apuntar), flags booleanos para cada slot de habilidad y un flag de interaccion. Tambien hay un array de comandos de UI para cosas como comprar objetos, equipar equipo o vender botin.
Todo el struct serializa a unos 7 bytes. Eso es todo lo que un jugador envia por tick. No hay datos de posicion, no hay estado de salud, no hay contadores de dano. Solo lo que presionaste.
El Tick de Simulacion
El host recopila los comandos de input de todos los jugadores conectados y ensambla un SimulationTick, un struct que contiene el numero de tick y el array de inputs de todos los jugadores. Este tick se comprime (usando la compresion GZip integrada de C#) y se transmite a todos los clientes. Cada cliente recibe el tick, lo descomprime y ejecuta la simulacion para ese frame exactamente de la misma manera.
Si un input de un jugador llega tarde, el host usa prediccion: copia la velocidad de movimiento y la direccion de aim del ultimo tick conocido. No se adivinan las activaciones de habilidades porque predecir mal un lanzamiento de habilidad crearia divergencia de simulacion visible.
Delay de Input
Warman calcula el delay de input dinamicamente basandose en el ping one-way medido al host. La formula divide el ping por la duracion del tick (1000ms / tick rate) y redondea hacia arriba, luego suma un buffer de jitter configurable. A 20 ticks por segundo con 50ms de ping, son unos 2 ticks de delay, aproximadamente 100ms. La mayoria de jugadores no notan nada por debajo de 150ms. En solitario o como host sin jugadores conectados, el delay baja a 1 tick.
Transporte
La capa de red esta abstraida tras una interfaz ITransport con dos implementaciones. SteamTransport usa la red de relays de Valve para NAT traversal (sin necesidad de abrir puertos). LiteNetTransport usa LiteNetLib para UDP directo en builds sin Steam. La eleccion de transporte es automatica. Si el jugador lanzo a traves de Steam, se usa el transporte de Steam. De lo contrario, cae a LiteNetLib.
Cuando el Host se Desconecta
Si el host se desconecta durante una partida multijugador, la simulacion se pausa. Cada cliente muestra un mensaje de "esperando reconexion" y espera que los ticks se reanuden. Si el host vuelve, la simulacion continua donde se quedo, no se pierde ningun estado. Si el timeout expira, los clientes se desconectan. En solitario, si no hay partida multijugador, no hay nada que continuar, asi que el cliente se desconecta normalmente.