🐍 Programando el juego de Snake con Go y WASM
Llevo tiempo con ganas de programar el juego de la serpiente (snake) por dos motivos:
- Practicar programación: estructuras de datos y mejorar en Go.
- Me pasaba horas jugando cuando estaba en la universidad, y no hay ningún juego que me guste su jugabilidad.
Así que, ¿por qué no divertirnos un rato?
Interfaz gráfica
Al principio pensaba en crear un juego en la línea de comandos (CLI), pero cuando estuve haciendo algunas pruebas, me pareció algo engorroso cómo obtener los eventos de teclado (KeyPress) del usuario, así como hacer las actualizaciones en pantalla. Por tanto descarté la idea.
Como me apetecía hacerlo en Go, encontré este motor gráfico 2D que es bastante sencillo (ebiteen).
Este paquete únicamente te permite dibujar elementos en pantalla, nada de colisiones ni otros efectos, por lo que es justo lo que buscaba. Además me sorprendió que en su web tienen demos desplegadas usando Web Assembly (WASM).
La documentación es algo escasa, pero viendo algún ejemplo encuentras las tres funciones que necesitas para que el juego funcione.
Estructuras de datos
En esta sección vamos a ver cómo he diseñado las estructuras de datos con las que funciona el juego.
No voy a entrar en detalle de todas, simplemente comentaré las que considero más relevantes.
Snake
La serpiente es la parte que más dudas tenía, pero al final era bastante sencillo. He utilizado un array, de forma que en cada iteración del juego el último bloque de la serpiente pasa a ser el primero, desplazándose en la dirección que el jugador quiere moverse.
Nota: un slice de Go quizá no es la manera más eficiente, ya que hago un pop y un push de elementos, lo que puede implicar reservas de memoria innecesarias. Quizá una lista u otra estructura de datos puede optimizarlo.
Comida
La comida es simplemente un "Punto" con coordenadas X e Y. Se genera de forma aleatoria en cualquier posición del tablero. Esto tiene una pega, y es que puede aparecer en la propia serpiente.
Nota: pensar en un algoritmo que permita que la comida apareza de forma aleatoria pero no en la serpiente es interesante. Una opción es generarla de forma aleatoria N veces, pero conforme la serpiente creza habrá menos espacio, por lo que no me gusta esta solución.
Juego
Contiene las dimensiones del tablero, la serpiente y la comida. Además mantiene información como la puntuación y otros datos de la partida. Un dato importante es el tamaño del bloque con el que se pinta la serpiente, ya que este es el que delimita el "grid" por el que la serpiente puede moverse.
El tablero tiene un ancho y un alto, se incrementa de 1x1 pero sólo permite girar a la serpiente cuando la coordenada en la que está es múltiplo del tamaño del bloque. Esto tiene una pega respecto al input del usuario, ya que no es inmediato. La solución que he realizado es guardar la última tecla pulsada y comprobarla en cada iteración. El resultado es muy bueno.
También se puede regular la velocidad de la serpiente, cambiando el número de updates del engine por minuto, pero no se puede cambiar desde la interfaz.
Interfaz de usuario
Por último hablar sobre cómo se renderizan las estructuras de datos anteriores.
Dibujado
Primero dibujo los márgenes y la información del juego (número de puntos y estado de pausa). Después, pinto la serpiente y la comida, transformando el sistema de coordenadas del juego en el del renderizado.
Por último pinto los bloques que respresantan la serpiente y la comida, usando el tamaño de bloque configurado.
Colisiones
El cálculo de las colisiones es sencillo, ya que únicamente hay que tomar la cabeza de la serpiente y calcular:
- Si se sale de los márgenes del tablero por los bordes => es tan sencillo como comprobar si la posición de la serpiente es mayor/menor que los límites del tablero.
- Si está en la comida => comparando la posición [X, Y].
- Si se ha chocado contra sí misma => recorriendo la serpiente y comparando [X, Y].
Para que quede más realista, el cómputo lo realizo calculando la distancia euclídea, pero ajustando el sistema de coordenadas de origen con el tamaño del bloque.
Distribución del juego
Una vez tenemos el juego listo, hay que compilarlo para que la gente pueda jugar a él, pero claro, al estar hecho en Go la única opción es generar varios ejecutables (uno para Mac, otro Windows, etc.).
Esta idea no me gustaba demasiado, por lo que me acordé que Go permite compilación a WebAssembly.
Lo compilé, generé un index.html que cargue este fichero, y boom. El snake está listo en el navegador.
En este post no voy a entrar mucho en la parte de Web Assembly. Haré otro post para hablar de él.
Ventajas:
- Lenguajes como Go, C/C++ o Rust permiten generar rutinas WASM ejecutables en muchos sitios.
Desventajas:
- El ejecutable ocupa varios megabytes (en mi caso 5MB) debido a que lleva las rutinas necesarias para ejecutar el lenguaje en la web.
Por tanto, cuando vayas a usar Web Assembly tienes que sopesar este punto, y ver si otros motivos (como el rendimiento) te merecen la pena.
Resultado
El juego podéis probarlo en snake.pirobits.com y podéis ver el código en GitHub.
Posibles mejoras:
- Añadir selector de dificultad (UI).
- Añadir temas (colores) (UI).
- Mejorar algunas estrucutras de datos para que sea más eficiente (performance).
- Generar la comida siempre fuera del cuerpo de la serpiente (bug).
Si te ha resultado útil este artículo agradecería si te suscribes a mi newsletter. Recibirás contenido exclusivo de calidad y también me ayudarás enormemente. Cada suscripción apoya el trabajo que realizo y me permite conocer mejor los temas que te interesan, de forma que puedo mejorar los conocimientos que comparto contigo.