Parte: 7 — Multijugador y networking · Fuente: Valve, "Source Multiplayer Networking" + Gabriel Gambetta, "Fast-Paced Multiplayer" ⏱️ Duración estimada: 55 min · Nivel: Avanzado
Eliminar la sensación de "input pegajoso" cuando todo espera al servidor. Vas a hacer que el cliente prediga su propio movimiento de inmediato, mientras el servidor sigue siendo la autoridad final. Cuando llega el estado autoritativo, el cliente reconcilia: coloca su avatar donde dice el servidor y reaplica los inputs que aún no habían sido confirmados. Para ello sellarás cada input con un número de tick y mantendrás un buffer de inputs no confirmados.
Al finalizar, el alumno podrá:
| # | Tema | Por qué importa |
|---|---|---|
| 1 | Latencia y round-trip | Es lo que hace que el input se sienta lento |
| 2 | Predicción del cliente | El avatar responde ya, sin esperar al servidor |
| 3 | Sellado de input por tick | Permite saber qué inputs confirmó el servidor |
| 4 | Buffer de inputs pendientes | Guarda lo no confirmado para reaplicarlo |
| 5 | Servidor autoritativo | La verdad final la tiene el servidor |
| 6 | Reconciliación (rewind & replay) | Corrige la predicción sin romper el control |
| 7 | Simulación determinista del paso | Predicción y servidor deben mover igual |
Godot 4.x con dos instancias (Debug → Run Multiple Instances). Partimos del avatar de la clase 144. Introducimos una función pura aplicar_input() compartida por cliente y servidor para garantizar el mismo movimiento. Para notar la mejora conviene simular latencia: en el diálogo Debug → Network Profiler o añadiendo un retardo artificial al procesar RPCs. Lee la explicación visual de Gabriel Gambetta sobre predicción y reconciliación.
1. Un paso de movimiento determinista. Cliente y servidor deben mover igual. Extrae el paso a una función pura.
const VELOCIDAD := 220.0
# Movimiento puro: mismos datos -> mismo resultado en cliente y servidor
static func aplicar_input(pos: Vector2, dir: Vector2, delta: float) -> Vector2:
return pos + dir * VELOCIDAD * delta
2. El cliente predice y sella el input. En cada tick físico, el cliente genera un input con número de tick, lo aplica ya (predicción) y lo guarda en el buffer antes de mandarlo al servidor.
extends CharacterBody2D
var tick := 0
var inputs_pendientes: Array = [] # cada elemento: {tick, dir}
var pos_predicha := Vector2.ZERO
func _physics_process(delta: float) -> void:
if not is_multiplayer_authority():
return
tick += 1
var dir := Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down")
var entrada := {"tick": tick, "dir": dir}
inputs_pendientes.append(entrada)
# Predicción: muevo YA, sin esperar al servidor
pos_predicha = aplicar_input(pos_predicha, dir, delta)
position = pos_predicha
enviar_input.rpc_id(1, tick, dir) # al servidor (id 1)
3. El servidor procesa y responde el estado autoritativo. Simula con la misma función y devuelve la posición junto al último tick procesado.
@rpc("any_peer", "call_remote", "unreliable_ordered")
func enviar_input(cli_tick: int, dir: Vector2) -> void:
if not multiplayer.is_server():
return
var delta := 1.0 / Engine.physics_ticks_per_second
pos_predicha = aplicar_input(pos_predicha, dir, delta)
var emisor := multiplayer.get_remote_sender_id()
# Devuelvo estado oficial: posición + último tick que apliqué
recibir_estado.rpc_id(emisor, pos_predicha, cli_tick)
4. El cliente reconcilia. Al llegar el estado, descarta los inputs ya confirmados y reaplica los que quedan sobre la posición autoritativa.
@rpc("authority", "call_remote", "unreliable_ordered")
func recibir_estado(pos_servidor: Vector2, tick_confirmado: int) -> void:
# 1) descarto lo que el servidor ya confirmó
inputs_pendientes = inputs_pendientes.filter(
func(e): return e["tick"] > tick_confirmado)
# 2) parto de la verdad del servidor
var pos := pos_servidor
var delta := 1.0 / Engine.physics_ticks_per_second
# 3) reaplico lo aún no confirmado
for e in inputs_pendientes:
pos = aplicar_input(pos, e["dir"], delta)
pos_predicha = pos
position = pos
5. Compruébalo. Con dos instancias y algo de latencia simulada, el avatar propio responde al instante (predicción) y, cuando el servidor corrige, no da un tirón visible porque los inputs pendientes se reaplican. Si desactivas la reconciliación (paso 4), verás cómo el avatar se "pelea" con el servidor y retrocede.
rpc_id con un Timer) y observa la predicción con y sin reconciliación.pos_predicha y pos_servidor en cada reconciliación e imprímelo.reliable y comenta el efecto en la sensación de control.dir de módulo mayor que 1 (primer sanity check).tick local y el tick_confirmado para visualizar cuánto va por detrás el servidor.Implementa predicción + reconciliación para un avatar con latencia simulada de ~120 ms ida/vuelta. El input debe sentirse inmediato y las correcciones del servidor no deben producir tirones visibles.
Criterio de aceptación: con la latencia activa, mover el avatar responde en el mismo frame; al desactivar la reconciliación se aprecia un retroceso/tirón claro, y al reactivarla desaparece; el error medio reconciliado tiende a cero cuando el cliente y el servidor no divergen.
| Síntoma | Causa y arreglo |
|---|---|
| El avatar da tirones al corregir | No reaplicas los inputs pendientes tras colocar la posición del servidor |
| Predicción y servidor divergen siempre | La función de paso no es determinista (usan delta distinto). Usa un delta fijo en ambos |
| El avatar avanza el doble | Predices y además aplicas el estado del servidor sin descartar inputs confirmados |
| Input laggeado pese a predecir | Estás esperando el estado antes de mover; predice en el mismo tick del input |
| Buffer crece sin fin | Nunca filtras por tick_confirmado; recorta lo ya confirmado |
¿Por qué se siente lento sin predicción? Porque el avatar no se mueve hasta el round-trip completo (cliente→servidor→cliente). Con 100 ms de RTT notas ese retraso en cada pulsación.
¿La predicción abre la puerta a trampas? No, porque el servidor sigue siendo autoritativo: valida los inputs y su estado gana. La predicción es solo cosmética/de sensación.
¿Qué pasa si el servidor y el cliente divergen mucho? La reconciliación colocará el avatar en la posición del servidor; si la divergencia es grande, se verá un salto. Por eso importa el determinismo.
¿Reconcilio en _process o _physics_process? La simulación va en _physics_process (tick fijo). La reconciliación se dispara al recibir el estado, pero opera sobre el mismo modelo de tick.
Clase 144 - Mover jugadores en red: replicación de estado