Un juego de pesca cuyo backend tiene que durar.
Advertencia
Rod and Reef todavía no ha sido lanzado y no se pueden mostrar imágenes en vivo del juego ni de sus sistemas hasta que salga. Los artículos de abajo describen los sistemas sin videos ni GIFs.
Resumen. Rod and Reef es un juego de pesca con historia para Roblox, con cuatro islas, 530 quests, 117 archivos de diálogo y 74 líneas de voz grabadas. Es de código cerrado y sigue en desarrollo. Soy su lead developer: empecé en el cliente, haciendo UI, y ahora también me encargo del backend pesado, es decir, los datos del jugador, la suerte detrás de cada lance, las quests, los diálogos y las temporadas. Lo que une todo es que el juego tiene que seguir siendo mantenible cuando ya esté en vivo. El servidor decide, el cliente trabaja con copias de solo lectura y casi todo el contenido son datos que unas herramientas generan a partir de los documentos de diseño. Esta página es un índice: cada sección resume un sistema en pocas líneas y enlaza al artículo donde explico las decisiones y la matemática.
1. Mi rol
Soy el lead developer. Al principio me encargaba de todo el lado cliente, cada pieza de UI que el jugador ve y toca, y lo sigo haciendo, con preferencia por las interfaces que reaccionan a todo lo que haces: la ráfaga de partículas en cada acierto del minijuego de pesca, el texto de los diálogos que se escribe al ritmo de la voz, los subtítulos que siguen al actor palabra por palabra. Desde el ascenso también me encargo del backend que no puede fallar, que es todo lo que el servidor tiene que decidir o guardar: los datos, el sistema de suerte que decide qué pesca un lance, el progreso de las quests y las temporadas.
Como lead me hago responsable de que todo eso siga siendo mantenible y esté limpio técnicamente. Eso depende menos de un sistema en particular que de unas pocas reglas que se repiten en todos (sección 2).
2. Cómo está construido
El código es Luau vanilla con una estructura de servicios y controllers hecha para este juego. Descartamos frameworks como Knit porque a nuestra escala meten más overhead del que resuelven, y la organización de módulos mantiene explícito el límite entre servidor y cliente. El almacenamiento usa ProfileStore y la red usa ByteNet. Cuatro decisiones aparecen una y otra vez en los artículos de abajo.
- El servidor decide y el cliente guarda una copia. El perfil de quests es una sola clase que escribe en los datos del jugador en el servidor y es de solo lectura en el cliente, y los eventos viajan como paquetes binarios con un esquema por paquete.
- El contenido son datos. Las quests, los diálogos y sus capas son tablas, y unas herramientas generan la mayoría a partir de transcripciones por NPC de los documentos de diseño. El contenido generado y el escrito a mano viven en carpetas separadas y se combinan al cargar, así que regenerar nunca borra lo que alguien escribió a mano.
- Lo que puede correr dos veces tiene que ser seguro al correr dos veces. Las migraciones de datos corren en cada entrada, y cualquier servidor puede cerrar una temporada porque dos servidores que compiten por hacerlo escriben lo mismo.
- Se deriva en vez de mantener. Cuando algo depende del estado actual, lo recalculo desde ese estado en vez de confiar en que cada evento llegó una vez y en orden. El estado efectivo del jugador, el bloqueo de menús durante las cinemáticas y el corte de un bark cuando empieza una cinemática funcionan así.
3. Backend
- Datos del jugador. Una sesión de ProfileStore funciona como candado, así que solo un servidor escribe un perfil. Cada escritura modifica el perfil y avisa al cliente en la misma llamada, lo que permite que la copia del cliente, construida con una foto inicial más cambios por ruta, converja a la del servidor. Las migraciones son idempotentes, y los regalos llegan a jugadores que están en otro servidor o sin conexión.
- Temporadas. Temporadas de 45 días con cuatro tablas de clasificación y sin coordinación entre servidores. La temporada es una función del reloj, los contadores se reinician de forma perezosa y las lecturas pasan por cachés que deduplican peticiones en vuelo, así que mil jugadores abriendo una tabla cuestan una sola petición cada 30 segundos.
- Sistema de suerte. Cinco tiradas por lance, con la caña, el cebo, los amigos cercanos, el bote y los multiplicadores encima. Las probabilidades salen de dos escaleras con interpolación lineal, los umbrales de pity se derivan de la probabilidad natural y un análisis de proceso de renovación reproduce al decimal una simulación de 400 000 lances. Los precios de los cebos se derivaron para que cada cebo sea la mejor opción exactamente en su caña.
- Quests. 530 quests como datos, un grafo de prerequisitos que sobrevive a desactivar una quest, espacios activos clasificados a partir de los objetivos en vez de escritos a mano, y un diario ordenado por un estimado de cuántos lances cuesta cada quest.
- Diálogos. 117 archivos (80 conversaciones base y 37 capas) que un deserializador de dos pasadas convierte en grafos. El cliente recorre el grafo para mostrar la conversación y el servidor lo recorre en paralelo, así que solo acepta movimientos que de verdad le ofrecieron al jugador.
4. Cliente y UI
- Minijuego de pesca. Un dial donde una aguja y un objetivo giran en sentidos opuestos. La dificultad es una función pura de la caña y del pez, y la puntuación es un tira y afloja que, analizado como cadena de Markov, es más sensible a la habilidad que una carrera simple.
- Barks ambientales. El protagonista comenta lo que haces, y para eso hizo falta un
PlayerStateMachine(máquinas de estados en paralelo, una por sistema dueño) que responda “qué está haciendo el jugador ahora”. Un director elige las líneas de un mazo barajado para que no se repitan, y el mismo estado decide cuándo tienen que esperar los menús y los prompts.
5. Herramientas y librerías
- Subtítulos. Un pipeline en Python que transcribe cada audio con faster-whisper large-v3 y guarda los tiempos de cada palabra, empareja cada toma improvisada con su línea del guion, escribe un módulo de Luau por audio y sube los audios. En el juego, un reproductor guiado por reloj revela las palabras para que no acumule desfase.
- Spray. Mi librería de Luau para reproducir
ParticleEmitters dentro de la interfaz. Cada partícula es una función de un reloj, y con el mismo efecto usa entre 1.3 y 1.8 veces menos CPU por frame que Emitter2D. El juego la incluye como dependencia para los efectos del minijuego. - PrettyUI. Mi librería de menús dirigida por atributos: efectos de hover, animaciones, sonido y apilado de GUIs configurados en Studio sin scripting por botón. Los menús del juego la usan, y el bloqueo de menús que describe el artículo de los barks está construido encima de ella.
Este texto de Mastedore. Todos los derechos reservados.