Si alguna vez has organizado un cuestionario interactivo con una gran multitud en una sola sala de conferencias, sabes lo rápido que el Wi-Fi local puede saturarse. Cuando decenas de dispositivos luchan simultáneamente por el ancho de banda para conectarse a servidores remotos, incluso las mejores plataformas pueden verse obstaculizadas por la congestión de la red, lo que genera una latencia frustrante y círculos de carga.
Este desafío de conectividad es un obstáculo común en las reuniones y talleres de desarrolladores. La ironía es difícil de pasar por alto: cuando los participantes están sentados a solo unos pocos pies de distancia en la misma habitación, los datos no deberían tener que viajar hasta un servidor en la nube distante y regresar solo para sincronizar una pregunta en un grupo local de pantallas.
Así que construí KahootP2P. Es un juego de preguntas estilo Kahoot para iOS que funciona completamente mediante redes de pares (P2P) locales. Sin internet. Sin enrutador. Sin nube. Solo dispositivos que se comunican directamente entre sí, impulsados por la replicación P2P de Couchbase Lite Enterprise.
Esta publicación comparte la motivación detrás del proyecto, las decisiones de arquitectura involucradas y cómo Couchbase Lite hace que las partes más difíciles sean sorprendentemente simples.
El problema del modo multijugador dependiente de la nube
Kahoot es brillante. El sistema de puntuación basado en la velocidad genera verdadera tensión, la sencilla cuadrícula de respuestas de cuatro colores funciona en cualquier tamaño de pantalla y la tabla de clasificación después de cada pregunta mantiene a todos enganchados. Pero tiene una dependencia fundamental: absolutamente todas las interacciones pasan por internet.
Para un salón de clases con Wi-Fi dedicado, un enfoque dependiente de la nube funciona a la perfección. Sin embargo, en un salón de conferencias abarrotado donde 200 personas comparten la misma red, ese modelo de conexión centralizada puede provocar rápidamente una latencia severa y caídas de conectividad.
La latencia también importa. Kahoot otorga más puntos por respuestas más rápidas. Cuando tu respuesta correcta tarda 800 ms en llegar al servidor en lugar de 200 ms debido a la congestión de la red, literalmente estás perdiendo puntos sin que sea tu culpa. Esa no es una gran experiencia.
Por lo tanto, el objetivo era lograr una latencia de red medida en milisegundos de un solo dígito, no en cientos. La única manera de llegar ahí era eliminar internet por completo.
¿Por qué Couchbase Lite P2P?
En términos de experiencia multijugador sin conexión, el framework MultipeerConnectivity de Apple gestiona el descubrimiento y el envío básico de mensajes entre dispositivos iOS. Pero hay un detalle: está diseñado para topologías de malla donde cada dispositivo se comunica con todos los demás. Para un juego de preguntas con un anfitrión que controla el flujo, esto genera desafíos en cuanto a la consistencia y el orden.
Los parámetros más importantes incluyen:
- Una única fuente de la verdad – El dispositivo anfitrión debe ser la autoridad en cuanto a las puntuaciones, el tiempo de las preguntas y el estado del juego
- Sincronización automática de datos – Cuando el anfitrión escriba una nueva pregunta o actualice los puntajes, cada jugador debería verla sin que yo tenga que serializar y enrutar mensajes manualmente
- Entrega confiable – Si la respuesta de un jugador tarda un momento en sincronizarse, no debe perderse
La replicación de igual a igual de Couchbase Lite Enterprise encaja perfectamente con esto.
El URLEndpointListener permite que un dispositivo (el host) actúe como un destino de replicación. Los dispositivos de los jugadores se conectan a él utilizando un replicador estándar de Couchbase Lite en modo de envío y recepción. Esto proporciona una topología en estrella con el host en el centro, exactamente lo que necesita un juego con autoridad en el host.
La idea central es que no hay ninguna necesidad de construir un protocolo de mensajes personalizado. En lugar de pensar en términos de “enviar preguntas a todos los jugadores” y “recibir respuestas del jugador 3”, los documentos simplemente se escriben en la base de datos local, mientras que la capa de replicación se encarga de entregarlos en los lugares correctos.
Arquitectura: Documentos como Estado del Juego
Todo el estado del juego reside en documentos de Couchbase Lite. Así es como se modela:Juego Un solo documento que rastrea el título del juego, el código de unión, la fase (sala de espera/preguntando/cerrado/terminado) y el recuento de preguntas. Cuando el anfitrión cambia la fase, la replicación la envía a todos los jugadores.
Juego Un solo documento que rastrea el título del juego, el código de unión, la fase (sala de espera/preguntando/cerrado/terminado) y el recuento de preguntas. Cuando el anfitrión cambia la fase, la replicación la envía a todos los jugadores.
func sta
EstadoDeLaPregunta – Un documento que le indica a cada jugador en qué pregunta se encuentra y si todavía está abierta para recibir respuestas. El anfitrión actualiza este documento para avanzar en el juego.
Preguntas – Documentos individuales para cada pregunta, sincronizados del anfitrión a los jugadores cuando el juego comienza.
Respuestas – Cada jugador escribe su respuesta como un documento. La replicación la envía al anfitrión para su puntuación.
Resultados de respuestas – Después de la puntuación, el anfitrión escribe los documentos de resultados, los cuales se replican al jugador correspondiente.
Tabla de clasificación – Un solo documento con entradas clasificadas, actualizado por el anfitrión después de cada pregunta.
La belleza de este enfoque es que la sincronización es bidireccional y automática. Los jugadores envían las respuestas hacia arriba, el anfitrión envía los resultados hacia abajo, y Couchbase Lite se encarga de toda la resolución de conflictos y el ordenamiento.
Descubrimiento: Encontrar juegos sin servidor
Antes de que los dispositivos puedan replicar datos, necesitan encontrarse. Bonjour (el protocolo de red de configuración cero de Apple) se utiliza para esto.
Cuando el anfitrión inicia una partida, ocurren dos cosas:
- Un URLEndpointListener se inicia en un puerto disponible.
- Se publica un servicio Bonjour que anuncia ese puerto.
rtHosting(gameId: String) throws {
var listenerConfig = URLEndpointListenerConfiguration(
collections: [collection]
)
listenerConfig.port = 0 // Dejar que el SO elija un puerto
listenerConfig.disableTLS = true
let urlListener = URLEndpointListener(config: listenerConfig)
try urlListener.start()
// Anunciar a través de Bonjour para que los jugadores puedan encontrarnos
let service = NetService(
domain: "",
type: "_quizblitz._tcp.",
name: "QuizBlitz-\(gameId.prefix(8))",
port: Int32(urlListener.port ?? 0)
)
service.publish()
}
Del lado del jugador, un NetServiceBrowser busca el tipo de servicio _quizblitz._tcp. Cuando encuentra una partida, el jugador toca para unirse, y un replicador de Couchbase Lite se conecta al URLEndpointListener del host:
func connectToHost(host: String, port: Int) {
let url = URL(string: "ws://\(host):\(port)/\(db.name)")!
let targetEndpoint = URLEndpoint(url: url)
var config = ReplicatorConfiguration(
collections: [colConfig],
target: targetEndpoint
)
config.replicatorType = .pushAndPull
config.continuous = true
let repl = Replicator(config: config)
repl.start()
}
Eso es todo. Una vez que el replicador esté funcionando, los documentos fluyen automáticamente en ambas direcciones. Sin manejo manual de mensajes.
Puntuación: La velocidad importa
Al igual que en Kahoot, las respuestas correctas más rápidas obtienen más puntos. Pero aquí está la parte difícil: en una configuración P2P, no puedes simplemente usar marcas de tiempo de reloj de pared porque los dispositivos podrían no tener relojes sincronizados (y sin internet, no hay servidor NTP con el cual sincronizarse).
Esta solución utiliza el reloj monótono del host (mediante mach_continuous_time) como la única referencia de tiempo. Cuando el host abre una pregunta, registra su propio tiempo de actividad en nanosegundos. Cuando un documento de respuesta se replica en el host, este registra la hora de recepción.
let timeDelta = TimingService.elapsedSeconds(
from: startNs, to: receivedNs
)
let timeBonus = max(0, 1.0 - (timeDelta / Double(timeLimitSeconds)))
let points = isCorrect ? max(100, Int(1000.0 * timeBonus)) : 0
¿Es esto perfectamente justo? No. La latencia de replicación agrega unos milisegundos de ruido. Pero en la práctica, a través de una red Wi-Fi local, el retraso de replicación es inferior a 50 ms. Para un juego donde el límite de tiempo es de 15 segundos, ese retraso es insignificante. La persona que sabe la respuesta y toca más rápido seguirá ganando.
La puntuación funciona en una escala progresiva: responde al instante y obtén 1000 puntos. Responde en el último segundo y obtén 100 puntos. Responde mal y obtienes cero. Esto crea la misma sensación de urgencia que experimentas en Kahoot.
UI reactivo con Combine
La interfaz de usuario debe actualizarse en tiempo real a medida que los documentos cambian. La integración de Combine de Couchbase Lite hace que esto sea limpio:
db.collectionChangePublisher()
.receive(on: DispatchQueue.main)
.sink { [weak self] change in
self?.handleCollectionChange(change)
}
.store(in: &cancellables)
Este publicador se activa cuando cualquier documento de la colección cambia, ya sea por escrituras locales o por replicación entrante. El controlador comprueba si el cambio es relevante (el mismo ID de juego) y actualiza el estado correspondiente: nuevo jugador unido, respuesta recibida, tabla de clasificación actualizada.
Por el lado de SwiftUI, los motores son clases ObservableObject con propiedades @Published. Cuando el anfitrión escribe un nuevo documento QuestionState y este se replica a un jugador, el ClientEngine del jugador detecta el cambio a través del publicador de la colección, actualiza sus propiedades @Published y SwiftUI vuelve a renderizar la vista. Toda la cadena, desde que “el anfitrión presiona Siguiente pregunta” hasta que “el jugador ve la nueva pregunta”, toma menos de 100 ms en una red local.
La integración con Codable
Una de las cosas que hizo que el desarrollo fuera más rápido fue la compatibilidad con Codable de Couchbase Lite. En lugar de mapear manualmente entre documentos y objetos de Swift, los modelos simplemente podían definirse como clases Codable:
// Guardar un modelo directamente
try collection.save(from: game)
// Cargar de nuevo como un objeto tipado
let game = try collection.document(id: docId, as: Game.self)
El contenedor de propiedades @DocumentID asigna automáticamente el ID del documento de Couchbase Lite a una propiedad en el modelo. Esto hace posible trabajar con objetos Swift normales en todas partes y permite que el SDK maneje la serialización automáticamente.
Para las consultas, utilicé SQL++:
let sql = "SELECT META().id, * FROM _ WHERE gameId = '\(gameId)' AND displayName IS NOT MISSING"
let query = try database.createQuery(sql)
Puntos clave
Los documentos como estado de juego funcionan sorprendentemente bien. Al principio hubo escepticismo sobre el uso de un protocolo de replicación de bases de datos para la sincronización de juegos en tiempo real. En la práctica, la latencia es lo suficientemente baja como para que se sienta instantáneo, y obtienes entrega confiable, resolución de conflictos y persistencia gratis.
La topología en estrella es la mejor opción para los juegos con servidor autoritativo. Una red en malla suena bien, pero cuando un dispositivo necesita ser la autoridad en la puntuación y el flujo del juego, un modelo de estrella con el anfitrión en el centro es más sencillo y predecible.
No necesitas internet para la mayoría de los juegos multijugador local. Esto puede parecer obvio, pero es fácil recurrir por defecto a “crear un servidor” cuando los dispositivos que quieres conectar están literalmente en la misma habitación. La replicación P2P de Couchbase Lite elimina toda esa dependencia.
Bonjour discovery just works. Bonjour en iOS es totalmente sólido. Los dispositivos se encuentran entre 1 y 2 segundos de manera consistente.
Pruébalo
El código fuente completo está en GitHub: github.com/midopooler/KahootP2P
Necesitarás Couchbase Lite Enterprise (para el URLEndpointListener) y dos o más dispositivos iOS en la misma red local. Simplemente clona el repositorio, ejecuta xcodegen generate, compila y estarás jugando en un minuto.
Si estás creando cualquier tipo de experiencia multijugador local, colaborativa o que funcione sin conexión en dispositivos móviles, vale la pena considerar las capacidades P2P de Couchbase Lite. Elimina la necesidad de escribir una sola línea de código de red más allá de “iniciar el oyente” y “iniciar el replicador”, lo que ahorra una cantidad significativa de tiempo.
El juego es sencillo, pero el patrón se aplica mucho más allá de los cuestionarios, incluyendo la recolección de datos en campo con sincronización de equipos, la toma colaborativa de notas en modo avión y sistemas de punto de venta que siguen funcionando cuando se cae internet. Cada vez que necesitas que múltiples dispositivos compartan estado sin depender de la nube, esta misma arquitectura funciona.
Deja un comentario
Lo siento, debes estar conectado para publicar un comentario.