Hay una parte del desarrollo de un videojuego que casi nunca se ve.
No aparece en una captura de pantalla.
No es un personaje nuevo, ni una espada, ni un mapa, ni una animación.
Tampoco es particularmente atractiva cuando uno está pensando en el próximo contenido que quiere agregar.
Es la parte en la que el juego tiene que aprender a existir.
No solamente ejecutarse en mi computadora. No solamente funcionar cuando entro yo. No solamente responder correctamente mientras todo está conectado a localhost.
Tiene que existir en una máquina que está encendida en algún lugar, aceptar conexiones, mantener un mundo compartido, guardar información, enviar datos a los jugadores, sobrevivir a errores y, sobre todo, conseguir que todo eso ocurra sin que el jugador tenga que saber que está pasando.
Durante esta etapa de Isle Survivors hice exactamente eso. El resultado no fue solamente conseguir que el juego tuviera un servidor. Fue construir la primera versión de la infraestructura que realmente puede sostener al juego.
Y, quizás más importante todavía, descubrir que muchos de los problemas que parecían ser problemas de servidor en realidad estaban en otro lugar.
Esta es la historia de esa etapa.
De un prototipo a un juego conectado
Cuando Isle Survivors empezó, la preocupación principal era mucho más simple: ¿podemos hacer que el juego funcione?
Con el tiempo esa pregunta dejó de ser suficiente. Un battle royale necesita algo que un juego completamente local no necesita: todos los jugadores tienen que estar viendo una versión compatible del mismo mundo.
- Si un jugador está en una posición, el servidor tiene que saberlo.
- Si un árbol existe, tiene que existir para todos los jugadores que correspondan.
- Si un enemigo recibe daño, ese daño tiene que resolverse de forma autoritativa.
- Si alguien recoge un objeto, el resto del mundo no puede continuar creyendo que ese objeto sigue disponible.
- Y si dos jugadores hacen cosas al mismo tiempo, no podemos depender de cuál computadora terminó de procesar primero.
Por eso Isle Survivors terminó adoptando una arquitectura donde el servidor es autoritativo. El cliente muestra, predice y presenta el mundo. El servidor decide qué ocurrió realmente.
Esta decisión fue fundamental porque condicionó prácticamente todo lo que vino después.
La primera versión del servidor
La base terminó siendo deliberadamente sencilla. El backend corre sobre Node.js, utilizando Express para el servidor HTTP y Socket.IO para la comunicación en tiempo real. El cliente utiliza React y Vite, pero el juego propiamente dicho se renderiza mediante Canvas 2D. Para la persistencia utilizo Supabase.
La arquitectura terminó siendo aproximadamente la siguiente:

No intenté convertirlo desde el principio en una arquitectura gigantesca. Primero necesitaba algo que pudiera entender completamente: un proceso de Node.js, un game loop, un estado del mundo, conexiones mediante Socket.IO y un servidor que pudiera decir:
- Este jugador está acá.
- Este enemigo está acá.
- Este objeto existe.
- Este ataque ocurrió.
- Esta es la posición válida.
A partir de ahí empezaron los problemas interesantes.
El servidor funcionaba demasiado bien
Una de las primeras cosas que descubrí fue también una de las más engañosas. El servidor no era lento. De hecho, era sorprendentemente rápido. El game loop comenzó a medirse internamente y los resultados estaban alrededor de 1,1 a 1,4 milisegundos por tick.
El servidor funciona a 20 Hz. Eso significa un tick cada 50 ms. Por lo tanto, si un tick consume aproximadamente 1,2 ms, quedan alrededor de 48,8 ms disponibles antes del siguiente tick.
Tick disponible: 50 ms
Trabajo real: ~1,2 ms
Tiempo restante: ~48,8 msEso fue importante porque durante bastante tiempo era fácil mirar un juego online que se sentía entrecortado y pensar: “El servidor está lento.”
Pero los números contaban otra historia. El servidor estaba haciendo su trabajo rápidamente. El problema estaba en cómo se utilizaba esa información.
El primer enemigo: las colisiones
La primera gran optimización llegó por un problema clásico. Las entidades empezaron a crecer: jugadores, mobs, proyectiles, objetos, obstáculos. Y cada entidad podía necesitar comprobar su relación con otras entidades.
La implementación ingenua de este tipo de sistema puede terminar haciendo algo parecido a:
Jugador 1 → comprobar contra todos
Jugador 2 → comprobar contra todos
Jugador 3 → comprobar contra todos
...Cuando la cantidad de objetos aumenta, el número de comprobaciones crece muy rápidamente. El problema no es que una comprobación sea cara. El problema es hacer miles de comprobaciones que, en realidad, no tienen ninguna posibilidad de producir una colisión. Un jugador que está en el extremo norte de la isla no necesita comprobar si chocó con un mob que está en el extremo sur.
Así apareció una de las optimizaciones más importantes de todo el servidor: el spatial hash grid. La idea es dividir el mundo en celdas. En lugar de preguntarle al servidor “¿con cuál de todas las entidades del mundo puedo estar chocando?”, se le pregunta “¿qué entidades existen cerca de esta celda?”.

Si el jugador está en una celda determinada, solamente interesa consultar esa celda y las vecinas. Además, se terminaron utilizando estructuras espaciales separadas para distintos tipos de entidades: jugadores, mobs y entidades estáticas. Eso permite que cada sistema consulte únicamente lo que realmente necesita.
La diferencia conceptual es enorme. Pasamos de “compará contra todo” a “compará contra lo que puede estar cerca”.
Y esta misma filosofía terminó apareciendo nuevamente en otras partes del proyecto.
El segundo problema: enviar el mundo entero
Después apareció otro problema. Aunque el servidor pudiera calcular todo rápidamente, todavía había que enviar información. Y enviar información también cuesta.
La primera aproximación era demasiado sencilla: el servidor tenía información sobre el mundo y enviaba una gran cantidad de ese estado periódicamente. Eso significaba que árboles, cofres, obstáculos y otros elementos estáticos podían terminar formando parte de los datos enviados repetidamente.
Pero había una pregunta obvia: ¿por qué enviar veinte veces por segundo algo que no cambia veinte veces por segundo? Un árbol no necesita ser enviado nuevamente porque pasaron 50 milisegundos. Una roca tampoco. Un cofre tampoco. El mundo estático no tiene el mismo comportamiento que un jugador.
Entonces apareció otra separación fundamental: dinámico no es igual a estático. Los jugadores, mobs, proyectiles y otros elementos dinámicos necesitan actualizaciones frecuentes. Los elementos estáticos pueden distribuirse de otra manera.
Chunks
La solución fue dividir el mundo estático en chunks. En lugar de transmitir continuamente todos los elementos estáticos, el cliente puede solicitar la información de una zona cuando se acerca.

A partir de ese momento, el cliente ya conoce esos árboles, piedras y cofres. No tiene sentido seguir mandándoselos en cada tick. Esto tuvo un efecto importante sobre el tráfico: las actualizaciones frecuentes quedaron reservadas para lo que realmente necesitaba estar actualizado.
Viewport Culling
Pero todavía había otro problema. Incluso entre las entidades dinámicas, no todo tiene que ser enviado a todos. Imaginemos una isla enorme: un jugador está en una esquina, otro está muy lejos. Mandarle absolutamente todo lo que ocurre en la isla al primer jugador es desperdiciar ancho de banda. Además, el cliente no puede mostrar cosas que están fuera de su campo visual.
Entonces implementé viewport culling. El servidor calcula qué entidades están suficientemente cerca como para resultar relevantes para ese jugador.

El servidor mantiene el mundo completo. Pero cada cliente recibe principalmente lo que necesita ver. Esto redujo considerablemente la cantidad de datos que tenían que viajar. En las pruebas realizadas, el payload por tick terminó alrededor de 6–9 KB, dependiendo del estado del mundo. No es una cifra mágica ni definitiva para toda escala futura, pero fue una reducción enorme respecto a transmitir indiscriminadamente el estado completo.
No todas las entidades necesitan la misma información
Otra optimización surgió de algo todavía más sencillo: no todos necesitan saber lo mismo. Mi propio personaje necesita información completa sobre sí mismo: sus estadísticas, inventario, equipo, vida, progresión y otros datos que no necesito enviar de la misma manera a todos los demás jugadores.
Pero otro jugador que aparece en pantalla principalmente necesita:
- posición
- vida
- animación
- estado visual
No necesita conocer toda mi estructura interna. Esto llevó a utilizar una forma de delta/update simplification: el propio jugador recibe información más completa y los jugadores remotos reciben solamente lo necesario para representarlos. Es una optimización pequeña conceptualmente, pero muy importante cuando se multiplica por cada conexión.
Los mobs tampoco necesitaban pensar todo el tiempo
Después apareció un problema diferente: los mobs. Una IA puede ser relativamente barata cuando hay pocos enemigos. Pero una isla con muchos enemigos puede convertir cualquier cálculo innecesario en un problema. Especialmente cuando los enemigos tienen que:
- buscar jugadores
- calcular movimiento
- evitar obstáculos
- actualizar física
- resolver colisiones
La solución fue introducir un concepto sencillo: mob sleep. Si un enemigo no tiene jugadores cerca, no necesita comportarse como si estuviera en el centro de una batalla. Puede entrar en un estado de suspensión.

Mientras está dormido, se reduce el procesamiento de IA y física. Cuando un jugador vuelve a acercarse, el mob puede volver a activarse. La idea detrás de esto es importante: no optimizar solamente el código; optimizar también cuándo hace falta ejecutar el código.
Guardar también puede producir lag
Otro problema apareció al pensar en la persistencia. El servidor necesita guardar información. Pero guardar información no debe convertirse en parte crítica del game loop. Una estrategia sencilla habría sido: cada 30 segundos, guardar todos los jugadores, guardar todo junto.
El problema es que eso crea una ráfaga. Si hay muchos jugadores, todos pueden intentar guardarse prácticamente al mismo tiempo. Y aunque cada operación individual sea razonable, la suma puede producir un pico.
La solución fue una save queue. En lugar de guardar todo de golpe, se procesa de forma escalonada.

La idea es proteger el game loop. El juego debe continuar corriendo aunque la persistencia tenga trabajo pendiente. Esta es una de esas decisiones que no hacen que una captura de pantalla se vea mejor. Pero pueden hacer que una partida no se congele.
Medir antes de optimizar
En algún momento apareció una herramienta que terminó siendo tan importante como las optimizaciones mismas: el profiler del game loop. No quería simplemente tener un número que dijera “Tick: 1.2 ms”. Quería saber en qué se estaba gastando ese tiempo.
Así empezaron a medirse distintas etapas:
GridUpdate
MobAI
Physics
Network
NetworkBytes
TotalTickY también se estableció una alerta cuando un tick supera un umbral importante. La idea es sencilla:
si TotalTick > 40 ms
avisar¿Por qué 40 ms? Porque el tick completo tiene 50 ms disponibles. Si estamos empezando a consumir 40 ms, ya no estamos cómodos. Un profiler no arregla un problema. Pero permite dejar de adivinar. Y esa diferencia terminó siendo fundamental.
El falso lag
Y acá apareció uno de los descubrimientos más importantes de esta etapa. El servidor estaba funcionando aproximadamente a 20 Hz. El cliente estaba intentando dibujar a 60 FPS.
Servidor:
50 ms → snapshot
50 ms → snapshot
50 ms → snapshot
Cliente:
16,6 ms → frame
16,6 ms → frame
16,6 ms → frame
...Si simplemente dibujamos la posición recibida del servidor, tenemos algo así:
Frame 1 → posición A
Frame 2 → posición A
Frame 3 → posición A
Frame 4 → posición B
Frame 5 → posición B
Frame 6 → posición B
Frame 7 → posición CEl servidor está funcionando perfectamente. Pero visualmente el jugador parece moverse a saltos. Era un problema de frame pacing. Y eso fue una lección enorme: un servidor rápido no garantiza que el juego se sienta rápido.
Client-Side Prediction
La solución fue mover la respuesta visual del jugador local al cliente. Cuando el jugador presiona una dirección, el cliente ya conoce el input. No tiene sentido esperar a que el servidor responda para empezar a mover visualmente al personaje.

El jugador empieza a moverse inmediatamente. El servidor sigue siendo la autoridad. El cliente solamente está intentando predecir lo que sabe que probablemente va a suceder.
El primer intento salió mal
Y esta parte es importante porque no todo funcionó a la primera. La primera implementación tenía un error matemático. El servidor utiliza speed como una cantidad de movimiento por tick.
speed = 5.5
20 ticks por segundo
5.5 × 20 = 110 px/sEl cliente, en cambio, inicialmente estaba utilizando una fórmula que terminaba aplicando un multiplicador incorrecto relacionado con 60 FPS. Eso hacía que el cliente predijera una velocidad cercana a tres veces la velocidad real. El resultado fue inmediato: rubber-banding.
CLIENTE
───────►───────►───────►
SERVIDOR
────►
CORRECCIÓN
◄────El cliente avanzaba. El servidor decía: no, no avanzaste tanto. Y lo corregía. Era exactamente lo contrario de lo que queríamos. Así que volvimos a la matemática.
Unidades antes que fórmulas
El problema no era realmente deltaTime. El problema era no haber definido correctamente qué significaba speed. Eso fue una lección importante. Antes de escribir una fórmula de movimiento hay que responder: ¿qué unidad representa cada variable?
speed = píxeles por tick
tick = 50 ms
ticks = 20 por segundo
const speed_pps = localSpeed * 20;
const dt = Math.min(frameDelta / 1000, 0.05);
movimiento predicho: speed_pps * dtAhora la velocidad es independiente del frame rate. 60 FPS, 144 FPS o una caída temporal de FPS no cambia la velocidad física esperada.
El segundo problema: el servidor está atrasado
Arreglar la velocidad no era suficiente. Todavía existía otro problema. El snapshot del servidor que llega ahora representa un estado que ocurrió un poco antes. Mientras el cliente recibe ese snapshot, el jugador ya siguió caminando. Entonces comparar la posición actual del cliente contra la posición recibida del servidor es injusto. El servidor está describiendo el pasado. El cliente está describiendo el presente. Eso genera un error que en realidad no es un error.
La solución fue almacenar un pequeño historial local de posiciones.
positionHistory
t0 → x100 y200
t1 → x102 y200
t2 → x104 y200
t3 → x106 y200
t4 → x108 y200Cuando llega un snapshot del servidor, el cliente intenta determinar qué momento del historial corresponde aproximadamente con ese snapshot. En lugar de preguntar “¿dónde estoy ahora y dónde dice el servidor que estoy?”, pregunta “¿dónde creía estar cuando el servidor estaba calculando esa posición?”.
Es una diferencia enorme. Si en ese momento servidor y cliente coinciden en x104, la predicción fue correcta. Aunque ahora el jugador esté en x112, no hay ninguna razón para corregirlo.
El historial también tiene que limpiarse
Un historial que crece indefinidamente sería un problema. Por eso se limita temporalmente. La implementación mantiene aproximadamente los últimos 500 ms de información y elimina los datos antiguos. Esto cumple dos objetivos: mantener suficiente información para la reconciliación y evitar un crecimiento innecesario de memoria.
Es una pequeña estructura, pero representa una idea que apareció muchas veces durante esta etapa: cada sistema necesita saber cuándo dejar de guardar cosas.
El problema de las paredes
La predicción funcionaba bien mientras el jugador caminaba libremente. Pero apareció otro problema: el cliente no tenía por qué saber exactamente qué iba a bloquearlo.
CLIENTE
🧍 → → → 🌳
↓
atraviesa
SERVIDOR
🧍 → 🌳
XEl servidor rechazaba el movimiento. El cliente ya había avanzado. Y entonces aparecía una corrección. Eso producía una sensación horrible: el personaje parecía entrar en el árbol y después ser expulsado. Así que agregamos una versión local y ligera de las colisiones estáticas.
isWalkableLocal
La función local no intenta convertirse en un segundo servidor. No replica toda la física. No procesa mobs. No intenta resolver todas las interacciones dinámicas. Su objetivo es mucho más limitado: evitar que la predicción visual haga cosas que claramente no puede hacer.

Esto eliminó una gran cantidad de correcciones visuales contra árboles, piedras y otros obstáculos estáticos.
El problema de los hitboxes
Y entonces apareció otro detalle. Un detalle pequeño. Pero exactamente el tipo de detalle que puede consumir horas. Había colisiones que visualmente parecían correctas, pero no producían el feedback esperado de minado.
La razón estaba relacionada con la identificación de las entidades. Los árboles y otros obstáculos no estaban siendo tratados exactamente de la misma manera al momento de buscar su ID para el feedback visual. El jugador chocaba. El código detectaba el bloqueo. Pero el sistema que tenía que decir “este objeto fue golpeado” no siempre encontraba correctamente el objeto correspondiente.
El resultado era extraño: el jugador chocaba, pero el árbol no reaccionaba; no aparecía el indicador; no había shake. La solución fue asegurarnos de transportar correctamente el identificador original de cada entidad estática durante la detección y el renderizado.
Esto parece una cosa menor. Pero es exactamente el tipo de bug que aparece cuando varias capas del juego —física, representación y feedback— tienen que hablar entre sí.
Hacer que una colisión se sienta como una colisión
Una vez que la física funcionaba, apareció otra pregunta: ¿por qué debería una colisión sentirse tan fría? El jugador chocaba contra un árbol. El servidor correctamente impedía avanzar. Pero visualmente podía sentirse como:
camino
camino
camino
STOPEso funciona. Pero no tiene mucho game feel. Entonces separamos nuevamente dos conceptos: física real y respuesta visual. No quería modificar la posición autoritativa del jugador para simular un golpe, porque eso volvería a interferir con la predicción y la reconciliación. Así apareció el visualBump.
El knockback visual
Cuando el jugador colisiona con un obstáculo, la posición física no cambia. Se mantiene donde debe estar. Pero la posición utilizada para dibujar el sprite puede recibir temporalmente un pequeño desplazamiento.
posición física
│
▼
render offset
│
▼
posición dibujadaEl desplazamiento dura apenas unos milisegundos. Es pequeño. Pero suficiente para comunicar “acabas de golpear algo”. El servidor no necesita saber que el sprite se movió visualmente unos píxeles. La física sigue siendo correcta. El jugador solamente siente el impacto.
Y entonces apareció el minado
La misma colisión podía utilizarse para comunicar otra cosa. Si el jugador está chocando contra un árbol o una veta de mineral, probablemente no está intentando simplemente caminar. Está interactuando con el mundo.
Entonces agregamos feedback visual. Cuando el jugador colisiona con un recurso minable, el obstáculo recibe un pequeño shake, aparece un indicador de pico que se eleva ligeramente y desaparece después de un breve instante. Algo tan simple como:
⛏️
↑
╱ ╲
🌳
🧍cambia completamente la lectura de la interacción. Ahora el jugador entiende inmediatamente: “estoy golpeando este recurso”. No tuvimos que agregar una ventana gigante. No tuvimos que poner un texto permanente. No tuvimos que interrumpir la pantalla. Solamente una pequeña respuesta visual.
Esto terminó siendo otra lección: el feedback puede ser barato técnicamente y enorme perceptualmente.
La cámara
El jugador ya se movía inmediatamente. Pero la cámara todavía podía introducir una sensación extraña. La cámara tenía smoothing. La posición objetivo cambiaba rápidamente gracias a la predicción. Pero la cámara necesitaba alcanzar esa posición. El factor utilizado terminó ajustándose a aproximadamente lerp = 0.25.
La idea no era eliminar todo movimiento de cámara. Tampoco hacerla completamente rígida. Buscábamos un pequeño peso: algo que permitiera que el jugador se sintiera conectado al movimiento sin que la cámara pareciera tener latencia. Y nuevamente la distinción fue importante:
INPUT
↓
PLAYER
↓
PREDICTION
↓
CAMERA
La cámara puede suavizar. El input no.El resultado
Después de todas estas modificaciones, las métricas empezaron a contar una historia muy diferente.
Servidor: 20 Hz
Tick de procesamiento: ~1,1 – 1,4 ms
Cliente: ~60 FPS
RTT observado: 18 – 40 ms
Tráfico por tick: 6 – 9 KBPero la métrica más importante no era ninguna de esas. Era simplemente: ¿se siente bien?
Y sí. El jugador ya no esperaba al siguiente snapshot para comenzar a moverse. El servidor podía continuar siendo autoritativo y trabajar a 20 Hz. El cliente podía presentar el movimiento a 60 FPS. Las colisiones estáticas podían predecirse localmente. La reconciliación podía mirar hacia atrás en el historial en lugar de pelearse contra el presente. Y las interacciones con el mundo podían tener respuesta visual inmediata.
Una conclusión inesperada
Probablemente la conclusión más importante de toda esta etapa fue esta: 20 Hz no significa necesariamente que el juego se sienta a 20 FPS.
Al principio parecía que había que hacer que el servidor actualizara mucho más rápido. Quizás 30 Hz. Quizás 60 Hz. Quizás incluso más. Pero aumentar el tick rate no era necesariamente la solución al problema que teníamos. El servidor estaba calculando correctamente. El problema era cómo el cliente interpretaba esos snapshots.
Una arquitectura correcta permite que un servidor a 20 Hz y un cliente a 60 FPS convivan perfectamente. El servidor no tiene que dibujar. El cliente no tiene que decidir la verdad. Cada uno puede hacer aquello para lo que está diseñado.
El servidor llegó a una primera versión estable
Con esto, la infraestructura dejó de sentirse como un experimento. Ya existía una máquina dedicada, un proceso de Node.js ejecutándose continuamente, Nginx delante del servidor, el dominio play.islesurvivors.com, Socket.IO para la comunicación en tiempo real, persistencia mediante Supabase, PM2 para mantener el proceso y un pipeline de despliegue mediante GitHub Actions.
Y, sobre todo, existía una separación bastante clara entre las responsabilidades.

No es la arquitectura final de Isle Survivors. Probablemente nunca exista algo que pueda llamarse “arquitectura final”. Pero sí es la primera arquitectura que siento que puede sostener el juego que quiero construir.
Lo que todavía queda
Cerrar esta etapa no significa que el servidor esté terminado. Hay cosas que todavía quedan pendientes. Una de las más importantes es la persistencia global de los cambios realizados sobre los chunks: actualmente un recurso puede romperse y desaparecer del mundo durante la sesión. El siguiente paso será conseguir que ese cambio forme parte realmente del estado persistente del mundo.
También quedan las pruebas de carga más grandes. No quiero asumir que porque un servidor procesa un tick en 1,2 ms automáticamente puede sostener cualquier cantidad de jugadores. La única forma de saber cómo escala realmente es ponerlo bajo presión: decenas de jugadores, más entidades, más ataques, más movimiento, más tráfico. Y observar qué ocurre.
Pero eso ya pertenece a otra etapa. Porque hay algo que aprendí durante todo este proceso: no tiene sentido optimizar infinitamente un servidor que todavía no sabemos si necesita esa optimización. Primero necesitábamos una base sólida. Ahora la tenemos.
El final de una etapa
Cuando miro hacia atrás, esta etapa tuvo algo extraño. Durante semanas, gran parte del trabajo no produjo nada espectacular para mostrar. No apareció un nuevo personaje. No apareció un nuevo bioma. No apareció una espada legendaria. No apareció un jefe. Aparecieron cosas como:
Spatial Hash
Chunks
Culling
Queues
Profiler
Prediction
Reconciliation
Nginx
PM2
WebSocketsNo son cosas que normalmente aparecen en el tráiler de un videojuego. Pero son las cosas que permiten que ese videojuego exista. Y creo que esa es la diferencia entre tener un prototipo y empezar a construir un juego de verdad.
De servidor a mundo
Hay una idea que empezó a aparecer mientras hacía todo esto. Al principio pensaba en el servidor como una máquina que tenía que ejecutar el juego. Ahora lo veo de otra manera: el servidor es el lugar donde empieza a existir el mundo compartido.
El cliente puede equivocarse visualmente. Puede predecir. Puede interpolar. Puede hacer que un árbol tiemble. Puede hacer que aparezca un pico. Puede mover la cámara suavemente. Pero existe una instancia central que dice qué pasó realmente. Eso permite que Isle Survivors sea un mundo y no simplemente varias copias del mismo juego.
Y con esa base finalmente puedo dejar de dedicar toda mi atención a conseguir que el juego simplemente funcione. Ahora puedo empezar a dedicarla a conseguir que sea Isle Survivors.
¿Y ahora qué?
Este es probablemente el cambio más importante. Hasta ahora buena parte de mi trabajo estuvo concentrado detrás de la pantalla del jugador: infraestructura, networking, servidor, persistencia, optimización, rendimiento.
Ahora quiero volver a mirar hacia el otro lado: hacia lo que el jugador realmente ve. Porque un servidor perfectamente optimizado no sirve de mucho si el mundo que sostiene todavía no tiene identidad. Y ahí empieza la próxima etapa de Isle Survivors: los personajes, los sprites, los enemigos, las armas, las animaciones, los recursos, los escenarios, los objetos y los nuevos contenidos.
Y, especialmente, empezar a trabajar con artistas para transformar las ideas y prototipos que tengo en algo que realmente se parezca al mundo que imagino. Hasta ahora estuvimos construyendo los cimientos. Ahora quiero empezar a construir encima de ellos.
De la infraestructura al arte
Hay algo que me gusta de este momento del proyecto. Por primera vez, el servidor ya no es la parte más emocionante. Eso es bueno. Significa que dejó de ser el principal obstáculo. La infraestructura está ahí.
- play.islesurvivors.com está ahí.
- El servidor autoritativo está ahí.
- La comunicación en tiempo real está ahí.
- La predicción está ahí.
- Las optimizaciones están ahí.
- El mundo puede funcionar.
Ahora quiero que cuando alguien entre a ese mundo tenga ganas de quedarse. Y para eso hacen falta otras cosas: que un personaje tenga personalidad, que un árbol parezca un árbol de Isle Survivors, que una veta de mineral no sea simplemente un rectángulo con una textura, que un slime tenga una silueta reconocible, que un guerrero se sienta diferente de un arquero, que una isla tenga lugares que den ganas de explorar, que un objeto en el suelo se reconozca inmediatamente y que mirar la pantalla permita saber qué está pasando sin necesidad de leer una explicación. Ese trabajo empieza ahora.
Una isla todavía en construcción
Isle Survivors sigue siendo un juego en construcción. Y probablemente lo sea durante mucho tiempo. Pero hay una diferencia importante entre cómo empezó esta etapa y cómo termina.
Al principio estaba tratando de demostrar que podía existir un juego multijugador. Ahora ya no necesito preguntarme eso. La respuesta está corriendo en internet. Hay un servidor. Hay jugadores. Hay conexiones. Hay un mundo compartido. Hay movimiento. Hay combate. Hay recursos. Hay errores. Hay optimizaciones. Hay cosas que todavía faltan.
Y eso último también es importante. Porque ahora los problemas que quedan ya no son “¿podemos hacer que funcione?”. Son preguntas mucho más interesantes: ¿cómo queremos que se vea? ¿Cómo queremos que se sienta? ¿Qué personajes queremos que habiten esta isla? ¿Qué contenido va a hacer que cada partida sea diferente? ¿Cómo hacemos que Isle Survivors tenga una identidad propia?
Esas son las preguntas que quiero empezar a responder ahora.
Cerrar una etapa
Después de todo este proceso, hay algo que cambió bastante en mi forma de ver Isle Survivors. Al principio pensaba en el servidor como una parte del juego que simplemente tenía que funcionar: que los jugadores pudieran conectarse, que el personaje se moviera, que los enemigos atacaran, que los datos se guardaran, que play.islesurvivors.com estuviera online. Y listo.
Pero construir esa parte del juego terminó siendo bastante más complejo de lo que imaginaba. No porque Node.js no pudiera manejarlo. No porque el VPS fuera insuficiente. No porque necesitara una arquitectura gigantesca. El problema estaba en los detalles.
- En cómo se calculaba una colisión.
- En cuántas veces se enviaba un árbol por segundo.
- En qué información necesitaba realmente cada jugador.
- En qué cosas tenían que ocurrir en el servidor y cuáles podían resolverse visualmente en el cliente.
- En cómo una diferencia de unos pocos milisegundos podía cambiar completamente la sensación de movimiento.
- Y, sobre todo, en aprender a medir antes de asumir dónde estaba el problema.
Hoy Isle Survivors tiene una infraestructura mucho más sólida que la que tenía al comenzar esta etapa. El servidor es autoritativo. El game loop funciona a 20 Hz. Las entidades cercanas se gestionan mediante estructuras espaciales. El mundo está dividido para evitar enviar información innecesaria constantemente. Los enemigos que no necesitan procesarse pueden dormir. Los datos persistentes se guardan de manera escalonada. El cliente puede predecir el movimiento local sin esperar al siguiente snapshot. Las colisiones estáticas pueden anticiparse visualmente. Y las pequeñas interacciones, como golpear un árbol o una veta de mineral, pueden sentirse instantáneas sin convertir cada efecto visual en una operación de red.
No significa que el juego esté terminado. De hecho, falta muchísimo. Pero hay una diferencia enorme entre decir “algún día voy a tener un servidor funcionando” y poder abrir una página, entrar a una partida y jugar. Ese segundo momento ya ocurrió.
Lo que empezó como infraestructura terminó cambiando el juego
Una de las cosas que más me sorprendió durante todo este proceso fue que muchas de las decisiones que tomé pensando exclusivamente en rendimiento terminaron afectando directamente al diseño del juego.
- El sistema de chunks no solamente reduce tráfico: también define cómo se puede construir un mundo grande.
- El culling no solamente ahorra ancho de banda: también permite que haya muchos más elementos alrededor del jugador.
- La predicción del movimiento no solamente evita esperar al servidor: hace que caminar por la isla se sienta completamente diferente.
- Y el sistema de colisiones locales terminó abriendo la puerta a algo que al principio ni siquiera estaba relacionado con networking: el game feel.
Cuando el jugador choca contra un árbol, ahora puede sentir el impacto. El personaje recibe un pequeño rebote visual. El árbol puede sacudirse. Puede aparecer el indicador de minería. Todo eso sucede inmediatamente, sin esperar a que un paquete viaje hasta el servidor, sea procesado y vuelva. El servidor sigue teniendo la autoridad sobre lo que realmente ocurrió. Pero el jugador no tiene por qué sentir toda esa distancia.
Creo que esa es probablemente una de las lecciones más importantes que me llevo de esta etapa: la infraestructura no debería sentirse. El jugador no debería saber que existe un tick de 20 Hz. No debería pensar en Socket.IO. No debería saber dónde está el VPS. No debería importarle cuánto tarda una consulta a la base de datos. Debería simplemente caminar, golpear, minar, pelear, explorar y sentir que el juego responde.
Los números detrás de la isla
Durante semanas pasé muchísimo tiempo mirando números: 1,2 ms, 20 Hz, 60 FPS, 18–40 ms, 6–9 KB. Son números importantes. El profiler me permitió descubrir que el game loop estaba utilizando apenas una pequeña parte de los 50 milisegundos disponibles entre ticks. El culling y los chunks redujeron la cantidad de información que necesitaba viajar constantemente. La predicción permitió que el movimiento dejara de depender visualmente de esos 20 snapshots por segundo. Y las pruebas de red permitieron comprobar que el problema que inicialmente parecía ser rendimiento del servidor estaba, en gran parte, en cómo el cliente presentaba la información.
Pero al final hay otro número que no aparece en ningún profiler: 0. Cero segundos esperando para empezar a moverse. Ese era el objetivo: que el jugador apriete una tecla y el juego responda. Que el mundo se sienta vivo. Que el servidor pueda estar trabajando detrás de escena sin que el jugador tenga que pensar en él.
La parte que todavía falta
Cerrar esta etapa no significa que la infraestructura esté terminada para siempre. Todavía quedan cosas importantes.
- La persistencia completa de los cambios realizados sobre los chunks.
- Pruebas de carga con muchos jugadores simultáneos.
- Reconexiones.
- Más pruebas sobre el comportamiento de mobs y combate bajo carga.
- Y seguramente problemas que todavía ni siquiera sé que existen.
Eso también forma parte de desarrollar un juego online: cada solución abre nuevas preguntas. Pero hay una diferencia importante. Antes estaba intentando conseguir que Isle Survivors tuviera un servidor. Ahora tengo un servidor sobre el cual puedo seguir construyendo Isle Survivors. Y para mí, eso marca el final de esta etapa.
De la infraestructura a la identidad
Durante todo este proceso estuve construyendo una parte enorme del juego que el jugador nunca debería notar: servidores, sockets, chunks, culling, colisiones, persistencia, predicción, reconciliación, optimización, logs, métricas, deploys, pruebas. Todo eso era necesario.
Pero cuando entrás a play.islesurvivors.com, ninguna de esas cosas es lo primero que debería importar. Lo primero debería ser la isla. Y hasta ahora, la isla todavía no tiene la identidad visual que quiero.
- Los personajes necesitan dejar de ser solamente modelos funcionales y convertirse en personajes.
- Los enemigos necesitan tener personalidad.
- Las armas necesitan reconocerse a simple vista.
- Los recursos necesitan sentirse parte del mundo.
- Las animaciones necesitan transmitir peso.
- La interfaz necesita tener su propio lenguaje.
- Y todo necesita empezar a pertenecer al mismo universo.
Quiero llegar al punto en el que alguien vea una captura de pantalla sin leer el nombre del juego y pueda decir: “Eso es Isle Survivors.” Ese es el próximo desafío.
Lo que dejo en esta etapa
Durante esta etapa aprendí que el rendimiento del servidor no se mide solamente mirando cuánto tarda un tick.
- Que el ancho de banda importa.
- Que mandar información innecesaria también es un problema.
- Que una IA no debería pensar cuando no tiene nada que hacer.
- Que guardar datos puede convertirse en un pico si no se escalona.
- Que 20 Hz pueden sentirse como 60 FPS.
- Que una reconciliación mal planteada puede convertir una buena predicción en una experiencia horrible.
- Que un árbol que vibra durante unos pocos milisegundos puede comunicar más que una ventana entera de texto.
- Y que, muchas veces, el problema que uno cree tener no es realmente el problema que tiene.
Pero quizás la lección más importante fue otra. Durante mucho tiempo estuve mirando lo que había debajo de la isla. Ahora quiero volver a mirar la isla. Esta vez, no para hacer que funcione, sino para hacer que valga la pena explorarla.
Próximo capítulo
La infraestructura ya está. Ahora empieza otra clase de construcción. Después de semanas construyendo lo que el jugador no ve, llegó el momento de empezar a construir lo que sí va a ver: los personajes, los sprites, los enemigos, las armas, las armaduras, los recursos, las animaciones, los escenarios, los efectos y, sobre todo, el estilo visual que va a definir la identidad de Isle Survivors.
La próxima etapa también va a ser diferente. Ya no voy a estar mirando solamente logs, ticks, paquetes y milisegundos. Voy a empezar a mirar sprites, paletas, animaciones, diseños y pruebas de estilo. Y, por primera vez, a trabajar con artistas para transformar todos estos sistemas que estuvimos construyendo en algo que realmente tenga personalidad.
El servidor ya tiene dónde sostener la isla. Ahora toca construir la isla que queremos mostrar.
Nos vemos en el próximo Isle Journal.