Tabla de contenidos
¿El clásico Snake II, pero multijugador?
Hace diez años, programé en Unity una réplica del clásico Snake II de Nokia para Android y para iOS.
📲 Descárgala en Google Play
🍎 Descárgala en App Store
El juego gustó bastante, en general. Fue descargado por más de dos millones de personas en todo el mundo, y gané unos cuantos miles de euros, a pesar de haberlo programado por las noches después de la universidad. Fumando chocolate de 3€/g.
Hoy, mi cuerpo es un templo, y después de una década adquiriendo experiencia como desarrollador de software, he vuelto a programar el gameplay del juego, he mejorado las interfaces de usuario y, sobre todo, he añadido un modo multijugador multiplataforma entre Android e iOS para que juegues Snake II con tus amigos.
Soy Carlos Sala, desarrollador de software, y te voy a mostrar los pasos que he seguido para añadir el multijugador a un juego que inicialmente no estaba pensado para ello. Sin entrar demasiado en detalles aburridos de programación.
¡Comenzamos!
Diseñando el modo multijugador de Snake II
Antes de empezar a programar, es importante tener claro lo que vamos a hacer.
En este caso, queremos desarrollar un modo multijugador para Snake II. Así que vamos a pensar qué elementos deberíamos añadir, quitar o modificar del juego, tomando como referencia el modo individual.
Requisitos del modo individual

Por si nunca habéis jugado, en el juego original:
- El jugador elige una velocidad y un laberinto desde el menú de opciones.
- Entra a una partida y aparece una serpiente moviéndose a la velocidad que hemos elegido y el laberinto, si lo hay.
- En la esquina superior izquierda se encuentra la puntuación del jugador, que se incrementa a medida que atrapas puntos o animales con la serpiente.
- Después de atrapar cinco puntos, aparece un animal, que da una puntuación mayor al ser atrapado, pero tienes que hacerlo antes de que se acabe el tiempo y desaparezca. El contador de tiempo del animal aparece en la esquina superior derecha.
- Cuando la serpiente choca contra su cuerpo o el laberinto, reproduce una animación, termina la partida y el juego carga la escena de puntuación donde puedes ver las estadísticas de la partida, volver a jugar, consultar tu posición en el ranking, compartir tu puntuación o volver al menú.
Requisitos del modo multijugador

Esas son las especificaciones originales del juego. Y para convertirlo en multijugador, se me ha ocurrido realizar los siguientes cambios:
- Ahora hay dos jugadores, así que uno tendrá que crear la partida y otro se unirá a ella utilizando el código de la sala.
- Cada jugador tiene una velocidad y laberinto seleccionado diferente en su juego, por lo que estas opciones tendrán que ser elegidas antes de cada partida por el jugador que crea la sala.
- Cuando comience la partida, en lugar de una serpiente, aparecerán dos. Y para diferenciarlas sin añadir más colores al juego, la serpiente del rival se mostrará semitransparente.
- La velocidad de ambas serpientes será la que se ha seleccionado al crear la partida. Y el laberinto que aparecerá también.
- En la esquina superior izquierda se mostrará tu puntuación y en la esquina superior derecha la del rival.
- Al aparecer los animales, el contador de tiempo aparecerá ahora centrado arriba entre las dos puntuaciones. Ya que el sitio donde originalmente se mostraba está ahora ocupado por la puntuación del jugador rival.
- La partida multijugador también termina cuando una de las dos serpientes choca contra una serpiente o el laberinto, pero solo reproducirá la animación de game over la serpiente que choque, que será la que haya perdido.
- La pantalla final, mostrará el mensaje “You Win” o “You Lose”, en lugar de “Game Over”, dependiendo de si has ganado o perdido la partida. También aparecerá la puntuación de los dos jugadores, un botón para volver a jugar y otro para volver al menú.
Con esta lista de requisitos, ya podemos hacernos una idea del resultado que queremos conseguir. Así que ahora sí, toca modificar el código de Snake II que escribí hace diez años cuando estaba empezando a programar.
De juego individual a multijugador
La principal diferencia entre un juego individual y uno multijugador, es la aparición de un servidor para coordinar el estado de una partida entre varios dispositivos.
Puedes programar el servidor de tu juego en el lenguaje de programación que prefieras, pero yo voy a hacerlo en JavaScript utilizando el framework Colyseus. Que es una excelente librería para juegos multijugador con soporte para Unity. Os recomiendo sinceramente echarle un ojo si tenéis interés en desarrollar un juego multijugador.

A grandes rasgos, durante una partida multijugador de Snake debería ocurrir lo siguiente:
- Los jugadores envían los cambios de dirección que realizan al servidor.
- El servidor actualiza el estado del juego con los inputs.
- Y en el siguiente tick del juego, el servidor calcula los movimientos, las colisiones y envía el nuevo estado a los dispositivos para que actualicen lo que los jugadores ven en la pantalla.
Al abrir el código del juego que escribí hace diez años, me di cuenta de que tendría que reescribir prácticamente todo el juego para hacer funcionar el modo multijugador.
El código estaba demasiado acoplado
El primer problema es que la lógica del juego está escrita en un único archivo, llamado GameSceneManager, con 917 líneas, y totalmente acoplada a Unity y a la implementación del modo individual.

Además de añadir el modo multijugador, tenemos que conservar el modo individual intacto. Así que tenemos tres opciones:
- Llenar el archivo
GameSceneManagerde condicionales para que el juego haga una cosa u otra dependiendo del modo de juego en el que te encuentres. - Duplicar el archivo en
SingleGameSceneManageryMultiGameSceneManagery utilizar uno u otro dependiendo del modo. - La solución buena, que es: separar este archivo en diferentes sistemas, escribir interfaces que describan el comportamiento de estos sistemas y realizar una implementación de cada sistema por cada modo de juego.

De esta forma, cuando el jugador mueva a la serpiente, esta se moverá directamente si estamos jugando solos o enviará la nueva dirección al servidor del juego para que realice los cálculos, si estamos en una partida multijugador.
Una forma elegante de relacionar interfaces e implementaciones es utilizar un sistema de inyección de dependencias. En Unity existen varios, pero el que más he utilizado es Zenject, así que es el que voy a utilizar.

Para desacoplar la lógica del juego de Unity, de forma que podamos replicar el código de forma idéntica en el servidor, tenemos que utilizar una arquitectura limpia que separe en capas el código, según la responsabilidad que tengan.
No voy a atascarse aquí explicando todo sobre clean architecture, porque además es una arquitectura que cada programador implementa de forma diferente. Pero en pocas palabras, estamos aislando el game loop, que vamos a simular en el servidor, de los detalles de presentación de Unity como los GameObject, escenas y UI.
Después de unas semanas de sufrimiento sin precedentes, el juego estaba correctamente separado en capas, siguiendo una arquitectura limpia; el gameplay separado en sistemas, los sistemas en subsistemas y cada subsistema descrito por una interfaz implementada dos veces: una para jugar en local y otra en multijugador.
El juego utiliza el motor de físicas de Unity
El código actual del juego tiene otro problema por resolver antes de poder simular una partida en el servidor: no podemos utilizar el motor de físicas de Unity fuera de Unity.
Como este juego no estaba pensado para ser multijugador, la forma más sencilla de implementar los movimientos y colisiones, en su momento, fue hacer uso del motor de físicas de Unity con Rigidbodies y colliders.

Pero para que el modo multijugador funcione, el movimiento de los jugadores y las colisiones entre los diferentes elementos del juego deben ocurrir en el servidor. Así que tenemos que diseñar un sistema de movimiento y colisiones discreto que podamos utilizar tanto en Unity como en el servidor en JavaScript.
Afortunadamente, no estamos haciendo un juego con simulaciones complejas. Por lo que, con un sistema de cuadrícula en el que registramos las celdas que están ocupadas, será suficiente para comprobar si un jugador aumenta su puntuación en el siguiente movimiento o choca contra un obstáculo y se termina la partida.
El estado del juego es frágil
El tercer y último cambio que tengo que realizar sobre el código antiguo, es convertir el estado del juego en un registro de eventos cronológicos. Os explico por encima a qué me refiero.
La comunicación entre el cliente y el servidor consiste en mensajes transmitidos por WebSocket, con la mínima información posible.

En este juego, los mensajes que envían los jugadores al servidor son únicamente los cambios de dirección al utilizar los controles. Pero el servidor sí que tiene que comunicar a los jugadores cambios en el estado del juego más diversos como:
- La nueva posición de los jugadores en cada tick.
- Si un jugador ha atrapado algún punto y cuál es su puntuación actual.
- La posición y el tiempo restante de los puntos y animales que van apareciendo.
- Si uno o los dos jugadores chocan contra un obstáculo.
Cada uno de estos cambios del estado del juego se llama comúnmente delta y va acompañado de un timestamp o índice que permite ordenarlos cronológicamente.
namespace SnakeII.Gameplay.Domain
{
public abstract class GameStateDeltaEntity
{
public int timestamp;
public int index;
public GameStateDeltaType type;
public GameStateDeltaEntity()
{
this.timestamp = 0;
this.index = 0;
this.type = GameStateDeltaType.Unknown;
}
}
}Con una snapshot inicial y la lista de deltas podemos reconstruir el estado actual del juego tantas veces como queramos. Es así, por ejemplo, como algunos juegos multijugador como Brawl Stars permiten reproducir repeticiones de las partidas: aplicando los eventos en el orden y momento en el que se produjeron.

A este sistema lo llamaremos GameHistoryEngine y vamos a construirle también una ventana sencilla en el editor de Unity para hacer el seguimiento de los eventos que van ocurriendo durante una partida, en caso de que tengamos que depurar algún error.
En cada uno de los sistemas del juego, sustituimos los cambios en el estado del juego por emisión de eventos al registro del GameHistoryEngine y escribimos un manejador para aplicar cada cambio sobre el estado del juego.
Funciona como un reloj suizo.
Este motor de eventos sería una solución sobredimensionada si solo tuviéramos el modo individual, pero es ideal para el multijugador. Además, nos va a ayudar a tener un código simétrico en el cliente y en el servidor.
Los nuevos menús del juego
Hemos refactorizado el código del juego, para exponerlo en un museo, y hemos convertido el gameplay en un motor de eventos que funciona tanto en local como recibiendo eventos externos desde el servidor.

Ahora solo nos queda extender el menú del juego para que los jugadores puedan crear o unirse a una partida utilizando un código.
Al abrir la aplicación, se muestra al jugador la pantalla de inicio, después el primer menú, que hoy por hoy es bastante innecesario, pero era así en el juego original; y un segundo nivel de menú con secciones relacionadas solo con el gameplay.
En este segundo menú es donde añadiremos el botón “Multiplayer”. Justo debajo de “New game”.

Menú multijugador

El menú multijugador es muy simple, solo tiene dos opciones:
- Host game para crear una partida.
- Join game para unirte a una partida.

Al entrar en “Host game”, el jugador selecciona la velocidad de la serpiente, el laberinto al que quiere jugar y pulsa el botón “Host” para crear una partida y obtener un código que puede compartir con un amigo para invitarlo a jugar.
Además, el jugador que crea la partida puede cambiar las opciones del juego en cualquier momento antes de que la partida empiece.

Por otro lado, si entras en “Join game”, tendrás un campo de texto en el que puedes introducir el código que han compartido contigo y al pulsar “Join” te unirás a la partida. Una vez dentro, verás la velocidad y el laberinto que vas a jugar, pero no podrás cambiar las opciones de la partida, porque no has sido tú quien la ha creado.
Para empezar la partida, el jugador que la ha creado, pulsa el botón de “Start” después de que otro jugador se haya unido.
Una interfaz sencilla, pero funcional, más que suficiente para este tipo de juego.
Aprobada por nueve de cada diez dentistas.
Implementación del servidor del juego
La aplicación de Unity está lista. Ha llegado el momento de programar el servidor.
Una verdad incómoda sobre los juegos multijugador es que básicamente tienes que programar el juego dos veces: una en el cliente y otra en el servidor. Así que cuanto más presente lo tengas mientras programas el cliente, más fácil te será trasladar el game loop al servidor.
Creamos el repositorio para el código e instalamos las dependencias que vamos a necesitar, en nuestro caso vamos a programar el servidor en TypeScript con la librería Colyseus, como hemos mencionado antes.

Hacemos el primer commit y ponemos música apta para programar a alta velocidad.
Tener que escribir otra vez el gameplay del juego, encima en otro lenguaje de programación, sería una pesadilla difícil de soportar si no fuera porque tenemos una fabulosa arquitectura limpia en Unity que nos permite llevar al servidor el código de las capas dominio y aplicación del cliente, sin calentarnos la cabeza.
Eso sí, pasándolo de C# a TypeScript, previamente. Una tarea que básicamente hace ChatGPT por mí.

Aun siendo previsor, no he tenido en cuenta que en JavaScript no iba a tener la librería Zenject para gestionar las dependencias entre clases e interfaces, así que tenemos que adaptar esa parte del código a una de las librerías de inyección de dependencias más conocidas de JavaScript: Inversify.
Si tuviera que empezar otra vez, probablemente utilizaría C# también en el servidor para que el código fuera aún más simétrico entre las dos partes.
Gestión de salas de juego con Colyseus
Por último, nos queda explorar la librería Colyseus para conectar la simulación del juego a las salas que los jugadores crean desde el cliente.
Realmente, solo tengo buenas palabras para Colyseus y para su desarrollador, Endel Dreyer, porque tiene una comunidad y documentación excelentes que hacen que la experiencia de desarrollo sea inmejorable. Es un producto muy maduro y robusto.
Gracias a Colyseus, en pocas horas, he conseguido:
- Configurar las salas de mi juego para recibir la velocidad y laberinto que el host envía desde el cliente.
- Gestionar los diferentes mensajes que se intercambian entre el cliente y el servidor como cuando un jugador se une, uno sale de la sala, se cambian las opciones del juego, se solicita empezar una partida y algunas otras más.
- Personalizar el ID de las salas, para hacerlo más simple. Con 4 números o letras mayúsculas será suficiente y más cómodo de compartir con otras personas.
- Desplegar el servidor en producción utilizando Colyseus Cloud, el servicio autoescalable de despliegue de servidores Colyseus. Es una auténtica pasada tener el servidor desplegado en cinco minutos y poder consultar los logs de forma sencilla para cuando algo falle. Todo por 15$ al mes.
Testeando el modo multijugador
Ha llegado la hora de la verdad.

La aplicación de Unity está lista y el servidor desplegado en producción.
- ¿Habrá valido todo este esfuerzo la pena?
- ¿Será aceptablemente divertido juntar dos serpientes en la misma pantalla para jugar?
- ¿Surgirán problemas técnicos?
- ¿Soy un genio o un loco?
Vamos a probar el juego y salir de dudas.
Ya que los jugadores van a poder enfrentarse Android contra iOS, vamos a compilar la aplicación para ambos y probarla directamente en los dos sistemas operativos. No debería haber diferencias, teóricamente, porque Unity se encarga de convertir el código. Pero en el maravilloso mundo del desarrollo de software todo es posible.
Vamos a establecer un plan de pruebas con la siguiente lista de casos a comprobar:
- Un jugador puede crear una sala y otro unirse.
- La partida transcurre y termina con normalidad.
- Los jugadores pueden volver a jugar una partida.
- Uno de los jugadores abandona la sala antes, durante y después de la partida.
Un jugador puede crear una sala y otro unirse
El primer caso es el más básico: un jugador hace de anfitrión y otro de invitado en una partida.
Con el primer dispositivo, elegimos las opciones de la partida y la creamos para obtener el código que vamos a compartir.

Aquí ya tenemos un problema. Con la solución actual, tenemos que memorizar el código, salir del juego y escribirlo en una aplicación de mensajería como WhatsApp para compartirlo. Demasiado incómodo para el jugador.
Para solucionarlo, añadimos un botón nativo de compartir en cualquier aplicación del móvil para poder enviar el código directamente.

Ahora sí, creamos la partida con las opciones que queramos, compartimos el código con el otro jugador y, desde el segundo dispositivo, nos unimos utilizando ese código.
Estamos dentro de la partida.
La partida transcurre y termina con normalidad
Los dos jugadores están dentro de una partida, pero aún nos queda probar la parte más delicada: el gameplay.
En el modo multijugador, las acciones de los jugadores y los eventos del juego, que suceden en el servidor, se transmiten por la red a través del protocolo WebSocket. Esto puede generar problemas de latencia, mensajes perdidos cuando se corta la conexión y otros escenarios no previstos, que arruinen la experiencia de juego.

Empezamos la partida desde el dispositivo del anfitrión y, sorprendentemente, el juego va bastante fluido y no veo ningún error después de haber jugado varias partidas con diferentes velocidades.
Al chocar uno de los jugadores contra un obstáculo, la partida termina y ambos jugadores son enviados a la escena de puntuación.
Todo en orden con el segundo caso. La peor parte ya ha pasado.
Los jugadores pueden volver a jugar una partida
Desde la pantalla de puntuación, al terminar una partida, los jugadores pueden pulsar sobre la flecha de volver a jugar para repetir una partida con las mismas opciones.

Volvemos a jugar con los dos dispositivos y… hemos olvidado resetear el estado de la sala en el servidor, así que, al entrar a la partida de nuevo, vemos a los dos jugadores en el mismo estado en el que terminó la partida anterior.
La solución es bastante sencilla, dejar la sala en el estado inicial justo antes de empezar una partida. Ya sea la primera vez que juegas o la décima.
Desplegamos los cambios del servidor, volvemos a probar y ahora sí, podemos repetir las partidas tantas veces como queramos. Genial.
Uno de los jugadores abandona la sala
En un mundo ideal donde los usuarios no hacen cosas inesperadas, nuestro juego ya estaría funcionando y listo para salir a las tiendas de aplicaciones.
Pero la realidad es que en cualquier momento antes, durante y después de una partida multijugador, uno de los jugadores puede abandonar la sala pulsando el botón de volver al menú desde la interfaz, si su conexión a Internet se interrumpe o si cierra la aplicación.
¿Qué política vamos a seguir para controlar estos escenarios?
¿Cómo vamos a notificar al jugador de lo que ha sucedido?
Nunca he sido un gran fan de los diagramas, pero, mientras desarrollaba este nuevo modo para Snake II, he descubierto lo útiles que son para visualizar diferentes aspectos de sistemas más o menos complejos que no puedes mantener en la cabeza fácilmente.

Utilizando una máquina de estados de la sala, vemos todos los escenarios posibles y podemos definir qué queremos que suceda cuando un jugador abandona la sala, dependiendo de si la ha creado o se ha unido a ella:
- Si el jugador que crea la partida sale, en cualquier momento, se termina el flujo y se devuelve a los jugadores al menú.
- Si un jugador que se ha unido a la sala la abandona antes de que empiece la partida, la sala volverá al estado de espera hasta que otro jugador se una.
- Y a partir de que la partida empieza, si cualquiera de los jugadores sale, se termina el flujo y se devuelve a ambos jugadores al menú.
Además, cada vez que termine el flujo porque uno de los jugadores ha abandonado, se mostrará la siguiente pantalla para informar al jugador.

Sistema de salas multijugador a prueba de balas… ¡listo!
Los últimos retoques
Hemos completado la misión principal de esta actualización de Snake II, que era añadir un modo multijugador, pero vamos a aprovechar que hemos abierto el melón para dar las últimas pinceladas al juego.
Mejorando la interfaz de usuario
Por un lado, vamos a mejorar la interfaz de usuario de los menús.
La UI que hice hace diez años no se ajustaba bien en todos los móviles y tablets. Así que la ajustamos para que se vea bien en cualquier dispositivo.
Además, los botones son imágenes, concretamente dos para cada botón: una para el botón normal y otro para cuando está pulsado. Por lo que vamos a utilizar una fuente parecida a la que venía en los Nokia 3310 dentro de Unity y eliminamos todas esas imágenes innecesarias.
Añadiendo compras integradas
Por último, me gustaría añadir una compra integrada en la aplicación: que los usuarios puedan eliminar los anuncios después de cada partida.

El servidor del juego solo va a costarnos 15$ al mes, pero últimamente la aplicación no genera ni 10$, así que estaría bien por lo menos cubrir los gastos.
Instalamos el servicio de compras integradas de Unity desde el editor, configuramos el producto con un precio razonable para ambas tiendas de aplicaciones y preparamos una pantalla ofreciendo el producto que se mostrará a los usuarios cada tres partidas, por ejemplo.
Rápido e indoloro, los jugadores pueden eliminar los anuncios de por vida por menos de 2$.
Publicación en Google Play y App Store
La versión 3.0.0 de Snake II está lista.
Así que compilamos la aplicación para Android e iOS y subimos una nueva versión a las tiendas de aplicaciones.
Como hemos actualizado la interfaz del juego y añadido una nueva funcionalidad, el modo multijugador, aprovechamos y cambiamos las capturas de pantalla de las tiendas por unas más recientes.
Enviamos a revisión y después de unos días… ¡listo! Ya está disponible para todo el mundo.
📲 Descárgala en Google Play
🍎 Descárgala en App Store
Juega a Snake II con tus amigos
Con esta actualización de Snake II, un juego al que tengo tanto cariño, cierro una etapa de mi vida.

Hace años que me gustaría haber añadido el modo multiljugador, pero por diferentes motivos, como la falta de conocimientos técnicos o salir demasiado de fiesta, he necesitado diez años para poder llevarlo a cabo como se merece.
Probad el juego con vuestros amigos y me decís qué os parece. Si queréis mostrar vuestro apoyo, ya sabéis que por un precio asequible podéis eliminar los anuncios dentro del juego.
No sé si os gusta este contenido, así que házmelo saber para que prepare otros desarrollos parecidos. Yo, personalmente, he aprendido y me he divertido mucho haciéndolo.
