Parte: 7 — Multijugador y networking · Fuente: Glazer & Madhav, "Multiplayer Game Programming" + Glenn Fiedler, "Gaffer On Games" ⏱️ Duración estimada: 50 min · Nivel: Avanzado
Entender la capa de transporte sobre la que se construye todo juego en red antes de tocar la MultiplayerAPI. Al terminar sabrás por qué la inmensa mayoría de los juegos en tiempo real usan UDP y no TCP, qué significan latencia (RTT/ping), jitter, pérdida de paquetes y ancho de banda, y cómo ENet —el transporte que Godot usa por defecto— añade fiabilidad opcional sobre UDP. Montarás un servidor y un cliente ENet mínimos que imprimen la conexión, y medirás el RTT real entre dos instancias.
Al finalizar, el alumno podrá:
ENetMultiplayerPeer.| # | Tema | Por qué importa |
|---|---|---|
| 1 | Modelo de capas y transporte | Sitúa dónde vive el netcode de un juego |
| 2 | TCP: fiable, ordenado, con retransmisión | Explica su latencia impredecible bajo pérdida |
| 3 | UDP: datagramas sin garantías | Es la base de casi todo juego en tiempo real |
| 4 | Head-of-line blocking | Razón técnica clave para huir de TCP en acción |
| 5 | Latencia, RTT y ping | La métrica que define la sensación de "responder" |
| 6 | Jitter y pérdida de paquetes | Provocan el movimiento "a saltos" y desincronía |
| 7 | Ancho de banda y tasa de envío | Limita cuántas actualizaciones puedes mandar |
| 8 | ENet sobre UDP | Fiabilidad opcional por canal sin renunciar a UDP |
Necesitas Godot 4.x (estable) y la posibilidad de lanzar dos instancias del proyecto para simular servidor y cliente en la misma máquina (localhost / 127.0.0.1). En el editor puedes activar Debug → Run Multiple Instances → 2 para abrir dos ventanas de un golpe. Ten a mano la documentación de alto nivel de multijugador de Godot 4 y los artículos UDP vs TCP de Gaffer On Games. No hace falta abrir puertos en el router: todo el laboratorio corre en local.
Vamos a levantar un servidor/cliente ENet mínimo que imprima la conexión y, además, mida el RTT con un ping por RPC. Crea una escena Red.tscn con un nodo raíz Node llamado Red y adjúntale este script.
extends Node
const PUERTO := 9999
const MAX_CLIENTES := 8
func _ready() -> void:
# Conectamos señales de la MultiplayerAPI antes de crear el peer.
multiplayer.peer_connected.connect(_on_peer_conectado)
multiplayer.peer_disconnected.connect(_on_peer_desconectado)
multiplayer.connected_to_server.connect(_on_conectado_al_servidor)
multiplayer.connection_failed.connect(_on_conexion_fallida)
# El primer argumento de línea de comandos decide el rol.
if "--server" in OS.get_cmdline_args():
_iniciar_servidor()
else:
_iniciar_cliente()
func _iniciar_servidor() -> void:
var peer := ENetMultiplayerPeer.new()
var error := peer.create_server(PUERTO, MAX_CLIENTES)
if error != OK:
push_error("No se pudo abrir el servidor: %s" % error)
return
multiplayer.multiplayer_peer = peer
print("[SERVIDOR] Escuchando en el puerto %d. ID=%d" % [PUERTO, multiplayer.get_unique_id()])
func _iniciar_cliente() -> void:
var peer := ENetMultiplayerPeer.new()
var error := peer.create_client("127.0.0.1", PUERTO)
if error != OK:
push_error("No se pudo crear el cliente: %s" % error)
return
multiplayer.multiplayer_peer = peer
print("[CLIENTE] Intentando conectar a 127.0.0.1:%d..." % PUERTO)
func _on_peer_conectado(id: int) -> void:
print("Peer conectado: %d" % id)
func _on_peer_desconectado(id: int) -> void:
print("Peer desconectado: %d" % id)
func _on_conectado_al_servidor() -> void:
print("[CLIENTE] Conectado. Mi ID=%d" % multiplayer.get_unique_id())
# Al conectar, disparamos una medición de RTT.
_medir_rtt()
func _on_conexion_fallida() -> void:
print("[CLIENTE] Conexión fallida.")
Ahora añadimos la medición de RTT. La idea es: el cliente guarda una marca de tiempo, pide al servidor un "pong", y al recibir la respuesta calcula cuánto tardó el viaje completo.
var _t_envio_ms := 0
func _medir_rtt() -> void:
_t_envio_ms = Time.get_ticks_msec()
# Pedimos al servidor (id 1) que nos devuelva un pong.
ping.rpc_id(1)
@rpc("any_peer", "call_remote", "reliable")
func ping() -> void:
# Se ejecuta en el servidor: responde al emisor original.
var emisor := multiplayer.get_remote_sender_id()
pong.rpc_id(emisor)
@rpc("any_peer", "call_remote", "reliable")
func pong() -> void:
# Se ejecuta en el cliente: cerramos el cronómetro.
var rtt := Time.get_ticks_msec() - _t_envio_ms
print("[CLIENTE] RTT medido: %d ms" % rtt)
Cómo probarlo con dos instancias: ejecuta una instancia con el argumento --server (en Project Settings → Run o vía terminal godot --path . -- --server) y otra sin argumentos como cliente. Verás en la consola del servidor Peer conectado, en el cliente Conectado y el RTT medido. En localhost el RTT será de pocos milisegundos; ese mismo número entre dos máquinas reales sería tu ping.
Idea extra: para simular condiciones adversas puedes usar
Time.get_ticks_msec()y un temporizador que retrase artificialmente el envío delpong, o herramientas del sistema (clumsy en Windows,tc netemen Linux) para inyectar latencia y pérdida y observar cómo sube el RTT.
Timer y muestra el promedio de las últimas 5 mediciones.pong a "unreliable" y razona qué pasaría bajo pérdida de paquetes.multiplayer.get_peers().create_server puede fallar con ERR_ALREADY_IN_USE.Construye una pequeña "sala de espera de red": un servidor que acepte hasta 4 clientes, y una etiqueta en pantalla (en cada cliente) que muestre en tiempo real su ping al servidor actualizado cada segundo, más el número de jugadores conectados.
Criterio de aceptación: al abrir 3 instancias (1 servidor + 2 clientes), cada cliente muestra un ping numérico que se refresca cada segundo y el conteo "Jugadores: 2" correcto; al cerrar un cliente, el conteo baja a 1 en todos.
| Síntoma | Causa y arreglo |
|---|---|
multiplayer_peer no hace nada |
Olvidaste multiplayer.multiplayer_peer = peer; el peer se crea pero no se asigna |
| El cliente nunca conecta | El servidor no está corriendo o el puerto difiere; verifica que ambos usen el mismo PUERTO |
create_server devuelve error |
El puerto ya está en uso por otra instancia; cámbialo o cierra la anterior |
| RTT siempre 0 | Marcaste el tiempo después de recibir, no antes de enviar; guarda _t_envio_ms justo antes del rpc_id |
| El RPC no se ejecuta | La función no tiene la anotación @rpc o el nombre difiere entre peers |
Clase 137 - Capstone Parte 6: sistema de audio adaptativo
Clase 139 - Modelos de arquitectura: P2P, cliente-servidor y autoritativo