Videojuego Vanilla JavaScript

En este artículo vamos a explicar cómo programar un videojuego con Vanilla JavaScript, o dicho de otro modo, con JavaScript puro, sin plugins ni frameworks. Aprenderemos a manejar tanto los gráficos como la lógica del juego desde cero. Como suele ocurrir al empezar a programar, se nos irán ocurriendo más ideas para añadir al proyecto. Por eso, antes de comenzar, definamos unas premisas básicas:

  1. El juego debe ser finito: Partiendo de un planteamiento inicial, debemos llevarlo a cabo hasta el final. Todas las ideas adicionales que se nos ocurran deberán reservarse para otros juegos o futuras versiones, de lo contrario, nunca terminaríamos de programar un juego.
  2. Nuestro juego debe ser sencillo: Pero incluir los elementos básicos de todo videojuego tales como, un jugador, movimientos del jugador, enemigos, disparos, vidas del jugador, puntuaciones y el récord de la sesión.
  3. Estados básicos del juego: El juego debe contar con los estados básicos como la página de presentación, la parte jugable, pausar el juego, el «game over» y la opción de volver a jugar.
  4. Recursos gráficos y sonoros: En este ejemplo he querido que sean muy minimalistas, limitándonos a dos sonidos y un único «tileset» para todo el juego.

Videojuego Vanilla JavaScript: Meteor Rain

A partir de estas premisas básicas, he desarrollado el videojuego Meteor Rain con Vanilla JavaScript, el cual consiste en una nave espacial sobre un fondo estrellado que debe eliminar oleadas de meteoritos disparando contra ellos. Cada oleada es más rápida y difícil de eliminar que la anterior de modo que, al superar todas las oleadas se avanza un nivel, reiniciando las oleadas con una mayor dificultad en un ciclo infinito.

Si el jugador choca contra un meteorito, pierde una vida. Si no lo destruye, el meteorito rebota contra los márgenes y permanece dentro de la pantalla de juego. El objetivo del juego es que la dificultad aumente hasta que sea imposible sobrevivir, eliminando al jugador. El reto está en aguantar el mayor tiempo posible y superar el récord de la sesión actual.

Puedes jugar directamente a este juego haciendo «clic» en la imagen y después pulsando la tecla «y».

IMPORTANTE: Ahora que hemos presentado el juego y visto su jugabilidad, debo hacer una aclaración importante. Esto es solo un ejemplo básico de programación de un videojuego con Vanilla JavaScript. Sin embargo, NO es la mejor manera de programar videojuegos a gran escala. El objetivo principal de este artículo es mostrar cómo las distintas herramientas y controladores de teclado, sonido, mouse, etc. descritos en artículos anteriores, pueden ser útiles y eficaces para crear videojuegos como el que hemos presentado aquí.

Si quieres desarrollar videojuegos más complejos, es necesario utilizar una organización más estructurada y una programación orientada a objetos (POO) más avanzada, tal y como explico en mis libros sobre Programación de Videojuegos JavaScript.

Ahora que hemos hecho las presentaciones y aclarados estos conceptos sobre el código, veamos cómo está desarrollado el juego.

Recursos del videojuego Vanilla JavaScript

El «tileset» original que he utilizado para la programación de este videojuego en Vanilla JavaScript es el siguiente:

Tileset original del videojuego Meteor Rain
Imagen obtenida de freepick.es

A partir de esta imagen, comienza el proceso de diseño y selección de los elementos que utilizaremos en nuestro videojuego, con el siguiente resultado:

Tileset del videojuego vanilla javascript Meteor Rain

Esta organización del «tileset» es importante para la programación por varios motivos. Por una parte eliminamos el color de fondo y seleccionamos los elementos que vamos a utilizar. Por otra parte separamos los diferentes elementos del juego en filas y tamaños homogéneos con los siguientes objetivos:

  • La primera fila pertenece al jugador, donde el primer elemento es el disparo «estrella» y los siguientes son las naves del jugador. En este juego, seleccionamos la primera nave, pero con un simple cambio podemos optar por la segunda. Aunque la primera nave es estéticamente más atractiva, a veces las colisiones parecen «irreales» debido a su forma triangular. En ese caso, podemos cambiar a la segunda nave, que es más redondeada y mejora la sensación de colisión.
  • La segunda fila corresponde a los meteoritos, donde cada meteorito representa una oleada de izquierda a derecha.
  • Este diseño está vinculado a la programación, de modo que la estructura del tileset nos permite saber si un elemento gráfico pertenece al tipo «meteorito» simplemente conociendo su fila, lo cual será útil a nivel de programación.
  • Otro motivo es que el tipo de Movimiento en videojuegos nos obliga a diseñar gráficos en una dirección determinada.

En cuanto a los sonidos, únicamente utilizaremos dos para los efectos especiales:

  • Disparo: Cuando un disparo del jugador alcance un meteorito, se reproducirá el sonido «disparo» reforzando la sensación de «acierto» o «destrucción» del meteorito y aumentando la jugabilidad.
  • Colisión: Cuando el jugador choque contra un meteorito, activaremos el sonido «colisión» reforzando la sensación de destrucción de la nave.

Carpetas del videjuego Vanilla JavaScript

Nuestro espacio de trabajo, a la hora de programar cualquier videojuego, ya sea en Vanilla JavaScript o cualquier otro lenguaje, debe estar bien organizado con una estructura de carpetas similar a la siguiente:

  • css: Carpeta para guardar las hojas de estilo CSS del videojuego.
  • js: Carpeta para guardar los archivos JavaScript del videojuego.
  • img: Carpeta para guardar las imágenes del videojuego.
  • sound: Carpeta para guardar los sonidos del videojuego.

Esta estructura de carpetas nos servirá para guardar y cargar los diferentes tipos de archivos en nuestro documento HTML, en el cual añadiremos el lienzo <canvas> sobre el que desarrollaremos nuestro videojuego Meteor Rain.

<canvas id="canvasID" width="420" height="600"></canvas>

Debajo de nuestro lienzo, empezaremos a programar todo nuestro videojuego en Vanilla JavaScript y en menos de 200 líneas lo tendremos todo listo. Repito que mi intención en este artículo NO es dar una clase magistral sobre programación de videojuegos. Para eso tengo escritos mis libros, donde explico técnicas más complejas, aquí simplemente ofrezco unas nociones básicas sobre la lógica de un videojuego y cómo las herramientas y controladores de teclado, sonido y librería gráfica ayudan enormemente en su desarrollo. Puedes consultar todo el código fuente del juego en este enlace: Meteor Rain.

Controladores Vanilla JavaScript del videojuego

Los controladores del juego son clases programadas específicamente para ser utilizadas en nuestros videojuegos. La explicación completa de cada controlador la puedes encontrar en su artículo correspondiente. Aquí explicaré resumidamente su función:

let Sound = new audioClass();
let Keyboard = new keyboardClass();
let Graphics = new canvasAPI("canvasID");   
  • Sound: Es el controlador de sonido encargado de cargar y ejecutar los sonidos en el videojuego. Gracias a su biblioteca interna de sonidos, podremos reproducir cualquier sonido cargado mediante su nombre. Su utilización es esencial para reproducir sonido ambiente, música o efectos especiales.
  • Keyboard: Es el controlador de teclado encargado de capturar y mostrar las teclas pulsadas en el videojuego. Su funcionamiento está especialmente diseñado para videojuegos, permitiendo almacenar todas las teclas pulsadas simultáneamente y ejecutar acciones simultáneas con combinaciones de teclas.
  • Graphics: Es el controlador gráfico o librería gráfica, encargado de cargar y mostrar las imágenes en el videojuego, comprobar colisiones y realizar otras funciones gráficas. Se trata de un motor gráfico para programar videojuegos con Vanilla JavaScript, una librería extensa y compleja que se encuentra explicada en mis libros sobre Programación de Videojuegos JavaScript.

Utilizando estos controladores específicos tenemos una forma de comunicar nuestro juego con los recursos del ordenador (pantalla, sonido, teclado, etc.), adaptados a las necesidades específicas de los videojuegos y sin depender del sistema operativo.

Objeto Game del videojuego Vanilla JavaScript

El objeto Game es una forma básica de estructurar un videojuego en Vanilla JavaScript, su propósito es ilustrar conceptos fundamentales de lógica y estructura. Sin embargo, NO es la forma ideal de programar videojuegos más complejos. Puedes encontrar formas más avanzadas y eficientes de programar videojuegos en mis libros.

Dicho esto, el objeto Game contiene la lógica básica y principal de un videojuego, la cual se divide en varias propiedades y métodos lo cual nos da las siguientes ventajas:

  • Separación de responsabilidades: Cada método dentro del objeto Game se encarga de tareas específicas, lo que facilita la comprensión, el mantenimiento y la modificación del código.
  • Control del flujo del juego: Métodos como update(), reset(), y draw() gestionan el ciclo de vida del juego, garantizando un flujo de juego fluido y coherente.
  • Manejo eficiente de recursos: La separación de elementos del juego, en estrellas, disparos y enemigos, permite un control eficiente de los recursos y mejora el rendimiento general del juego.
  • Interacción con el usuario: Métodos como checkKeyboard() y drawInterfaz() gestionan la entrada del usuario y actualizan la interfaz del juego. Garantizando que las acciones del jugador y la información en pantalla respondan de manera efectiva a las entradas del usuario.
  • Modularidad y escalabilidad: La estructura modular del objeto Game permite añadir nuevas características o modificar las existentes con facilidad.

Esta organización del objeto Game proporciona una base para el desarrollo, mantenimiento y expansión de un videojuego, ofreciendo una perspectiva general para construir y mejorar el juego de manera eficiente. Repito de nuevo que esta NO es la forma ideal de programar un videojuego en Vanilla JavaScript, sólo es una muestra de la estructura básica de un videojuego y la potencia de los controladores de teclado, sonido y librería gráfica.

Dicho esto, el objeto Game se divide en varias propiedades y métodos que explicaremos a continuación.

let Game = {
  propiedades ...
  métodos ...
}

Propiedades

stars: [], 
player: {}, 
shoots: [], 
enemys: [], 
eType: 0, 
eTimer: 600, 
score: 0, 
level: 1, 
status: 'Splash'

La lógica o funcionamiento de cada una de estas propiedades es la siguiente:

  • stars: Array que contendrá estrellas de fondo a modo decorativo.
  • player: Objeto que contendrá los datos relativos al jugador.
  • shoots: Array donde guardaremos los disparos efectuados por el jugador.
  • enemys: Array con los diferentes tipos de meteoritos del juego.
  • eType: Especifica el tipo de meteorito actual.
  • eTimer: Es un temporizador para generar la salida de meteoritos.
  • score/level/status: Son variables para llevar el control de la mejor puntuación de la sesión actual (record), el nivel actual del juego (level) y el estado actual del juego (status).

Métodos

Los métodos son una parte esencial para programar un videojuego con Vanilla JavaScript y tener el código organizado por funciones básicas o elementales que se repiten continuamente en cada fotograma. Veamos cuales son:

addStars()

addStars() {
  let colors = ['#fff2', '#fff4', '#fff7', '#fffc'];
  this.stars.push(new Graphics.fillCircle({x: Graphics.rndFloor(Graphics.width), y: 0, speed: 0.5 + Graphics.rnd(1), radians: Graphics.radians(85 + Graphics.rndFloor(10)), radius: 0.5 + Graphics.rnd(1), color: colors[Graphics.rndFloor(4)]}));       
},

El método addStarts() añade el objeto gráfico (círculo relleno) con la librería gráfica, que simulará una estrella de fondo en el juego, con las propiedades (x, y, speed, radians, radius, color). Su tamaño, color y posición de salida será aleatoria y su dirección dirección será hacia abajo.

addPlayer()

addPlayer() {
  this.player = new Graphics.tile({sx: 100, sy: 0, sh: 100, sw: 100, x: 0, y: 0, width: 50, height: 50, speed: 10, angle: 270, lives: 3, score: 0, fTimer: 0, img: Graphics.img('tileset')});                            
},

El método addPlayer() crea e inicializa el objeto del jugador en el juego. Utiliza el método Graphics.tile para definir el aspecto gráfico del jugador, especificando la posición, el tamaño, la velocidad, el ángulo y otras propiedades clave. La imagen del jugador se carga desde el recurso ‘tileset’. El jugador comienza con 3 vidas, una puntuación de 0 y un temporizador de disparo (fTimer) inicializado en 0.

addEnemys()

addEnemys() {          
  if (this.eTimer > 0) this.eTimer--;
  else {
    this.eTimer = 300 + Graphics.rndFloor(600);
    if (this.eType < 4) this.eType++;
    else {
      this.level++;
      this.eType = 0;
    }
  }
  if (Graphics.rnd(100) < 10) this.enemys.push(new Graphics.tile({
    sx: (this.eType*100), sy: 100, sh: 100, sw: 100, x: Graphics.rndFloor(Graphics.width-50), y: 0, width: 50, height: 50, speed: (this.level + this.eType), angle: Graphics.rndFloor(360), radians: Graphics.radians(90 + (5 * this.eType) - Graphics.rndFloor(10 * this.eType)), img: Graphics.img('tileset')
  }));    
}, 

El método addEnemys() se encarga de agregar enemigos al juego. Primero, decrementa el temporizador eTimer, que controla la frecuencia de aparición de los enemigos. Cuando el temporizador llega a cero, se reinicia con un valor aleatorio y se ajusta el tipo de enemigo (eType). Si eType alcanza el máximo valor (4), se incrementa el nivel del juego y se reinicia el tipo de enemigo. Luego, con una probabilidad del 10%, la función añade un nuevo enemigo a la lista enemys. Cada enemigo se crea con propiedades aleatorias para su posición, velocidad, ángulo y dirección, utilizando el tileset para la representación gráfica.

init()

init() {
  this.level = 1;          
  this.eType = 0;
  this.eTimer = 600;
},    

El método init() reinicia los parámetros del juego a sus valores iniciales. Establece el nivel del juego en 1, el tipo de enemigo (eType) en 0, y el temporizador de enemigos (eTimer) en 600.

crash()

crash() {
  this.player.lives--; 
  if (this.player.lives > 0) this.status = 'Reset';
  else {
    this.status = 'GameOver';
    if (this.score < this.player.score) this.score = this.player.score;
  }
},

El método crash() maneja la colisión del jugador con un meteorito. Reduce el número de vidas del jugador en uno y, si aún tiene vidas restantes, cambia el estado del juego a ‘Reset’. Si no le quedan vidas, el estado cambia a ‘GameOver’ y se actualiza la puntuación más alta del juego si la puntuación actual del jugador es mayor que la anterior.

reset()

reset() {
  this.stars.length = 0;
  this.shoots.length = 0;
  this.enemys.length = 0;
  this.player.x = Graphics.width/2;
  this.player.y = Graphics.height - this.player.height - 20;          
}, 

El método reset() reinicia el estado del juego al comienzo de una nueva partida. Borra todos los elementos de las matrices stars, shoots y enemys, y coloca al jugador en el centro horizontal de la pantalla, ligeramente por encima del borde inferior.

checkKeyboard()

checkKeyboard() {
  if (Keyboard.key('ArrowUp')) this.player.y -= 5;
  if (Keyboard.key('ArrowDown')) this.player.y += 5;
  if (Keyboard.key('ArrowLeft')) this.player.x -= 5;
  if (Keyboard.key('ArrowRight')) this.player.x += 5; 
  if (this.player.x < 0) this.player.x = 0;
  if (this.player.x + this.player.width > Graphics.width) this.player.x = Graphics.width - this.player.width;		
  if (this.player.y < 0) this.player.y = 0;
  if (this.player.y + this.player.height > Graphics.height) this.player.y = Graphics.height - this.player.height;	   
  if (this.player.fTimer > 0) this.player.fTimer--;          
  if (Keyboard.key(' ') && this.player.fTimer == 0) {
    this.player.fTimer = 6;
    this.shoots.push(new Graphics.tile({
      sx: 0, sy: 0, sh: 100, sw: 100, x: this.player.x + (this.player.width-20)/2, y: this.player.y, width: 20, height: 20, speed: 10, angle: 0, radians: Graphics.radians(270), img: Graphics.img('tileset')
    }));        
  }
},

El método checkKeyboard() gestiona el movimiento del jugador y el disparo. Actualiza la posición del jugador según las teclas de flecha presionadas, asegurándose de que no se salga de los límites del canvas. También maneja el temporizador para disparos, permitiendo que el jugador dispare un proyectil cuando se presiona la barra espaciadora y el temporizador de disparo ha expirado.

collisions()

collisions() {
  for (let a=this.enemys.length-1; a>=0; a--) {
    let enemy = this.enemys[a];
    if (Graphics.hit(enemy, this.player)) {
      Sound.clone('crash', 100);
      this.status = 'Crash';      
      return;        
    }            
    for (let b=this.shoots.length-1; b>=0; b--) {
      let shoot = this.shoots[b];
      if (Graphics.hit(enemy, shoot)) {
        Sound.clone("shoot", 100);
        this.enemys.splice(a, 1);
        this.shoots.splice(b, 1);                
        this.player.score++;
        continue;
      }
    }
  }
},

El método collisions() gestiona las colisiones entre los enemigos, los disparos y el jugador. Recorre la lista de enemigos y verifica si alguno colisiona con el jugador, activando un sonido de colisión y cambiando el estado del juego si ocurre una colisión. Luego, verifica las colisiones entre los enemigos y los disparos, eliminando los enemigos y los disparos que colisionan, reproduciendo un sonido de disparo y aumentando la puntuación del jugador.

outOfCanvas(item)

outOfCanvas(item) {
  if (item.sy != undefined && item.sy > 0) {
    if (item.x < 0 || item.x + this.player.width > Graphics.width) item.radians = Math.PI - item.radians;
    if (item.y < 0 || item.y + this.player.height > Graphics.height) item.radians = -item.radians;
    return false;
  } else return Graphics.outOfCanvas(item);
},

El método outOfCanvas(item) gestiona los elementos que se salen de los límites del lienzo. Si el elemento tiene propiedades sy y sy > 0 (indicando que es un objeto gráfico de tipo meteorito), ajusta su dirección (radians) cuando toca los bordes del lienzo para que rebote en el borde. Si el elemento no tiene estas propiedades, simplemente verifica si está fuera del lienzo utilizando el método de la librería gráficaGraphics.outOfCanvas(item).

updateItems(items)

updateItems(items) {
  for (let i=items.length-1; i>=0; i--) {
    let item = items[i];
    if (item.angle != undefined) item.angle += item.speed;
    item.x += Math.cos(item.radians) * item.speed;
    item.y += Math.sin(item.radians) * item.speed;
    if (this.outOfCanvas(item)) items.splice(i, 1);      
  }
},

El método updateItems(items) actualiza la posición y el estado de los elementos en la lista items. Incrementa el ángulo de los elementos que tienen una propiedad angle, mueve cada elemento según su ángulo y velocidad, y elimina los elementos que están fuera del lienzo (canvas) llamando al método outOfCanvas().

update()

update() {            
  this.updateItems(this.stars);             
  this.updateItems(this.shoots);
  this.updateItems(this.enemys);                      
},

El método update() actualiza el estado de los elementos del juego. Llama al método updateItems() para procesar y actualizar la posición de las estrellas (this.stars), los disparos (this.shoots) y los enemigos (this.enemys).

drawInterfaz()

drawInterfaz() {
  Graphics.fillText({y: 40, size: 20, text: 'Level: ' + this.level});          
  Graphics.fillText({x: 20, y: 40, size: 20, align: 'left', text: 'Score: ' + this.player.score});
  if (this.score > 0) Graphics.fillText({x: 20, y: 60, size: 16, align: 'left', text: 'Record: ' + this.score});
  Graphics.fillText({x: Graphics.width - 20, y: 40, size: 20, align: 'right', text: 'Lives: ' + this.player.lives});
  Graphics.fillText({x: 12, y: Graphics.height - 12, size: 12, align: 'left', text: 'Items: ' + (1 + this.stars.length + this.enemys.length + this.shoots.length)});
}, 

El método drawInterfaz() se encarga de dibujar la interfaz del juego. Muestra el nivel actual, la puntuación del jugador, el récord (si es mayor que 0), las vidas restantes y el número total de elementos en el juego (estrellas, enemigos y disparos). Los textos se posicionan y se alinean en el lienzo según las especificaciones dadas.

drawItems(items)

drawItems(items) {
  for (let i=0; i<items.length; i++) {
    let item = items[i];
    if (item.angle != undefined) Graphics.rotate(Graphics.radians(item.angle), item);
    item.draw();
    Graphics.resetTransform();
  }
}, 

El método drawItems(items) se encarga de dibujar una lista de elementos en el lienzo. Para cada elemento en la lista, si el elemento tiene un ángulo definido, se aplica una rotación antes de dibujarlo. Después de dibujar cada elemento, se restablece la transformación gráfica para asegurar que las rotaciones no afecten a los elementos siguientes.

draw()

draw() {
  this.drawItems(this.stars);
  this.drawItems(this.enemys);
  this.drawItems(this.shoots);
  this.drawItems([this.player]);
  this.drawInterfaz();
}

El método draw() se encarga de renderizar todos los elementos del juego en el lienzo. Primero, dibuja las estrellas, los enemigos y los disparos utilizando la función drawItems(). Luego, dibuja al jugador, y finalmente, se encarga de renderizar la interfaz de usuario, mostrando información relevante como el nivel, la puntuación, el récord y las vidas restantes.

Precarga de imágenes y sonidos en Vanilla JS

Todos los juegos necesitan cargar los recursos necesarios antes de iniciarse, para garantizar un funcionamiento fluido con un alto rendimiento. En Vanilla JavaScript, también es necesario gestionar la carga de los recursos de nuestro videojuego.

En este juego específico, primero declaramos el objeto Game y luego cargamos los sonidos e imágenes. Esto se hace para evitar que un posible salto a animateScene ocurra antes de haber inicializado la variable Game.

Para este videojuego, solo necesitamos cargar dos sonidos, por lo que lo hacemos antes de iniciar el juego. En videojuegos más complejos, la carga de sonidos puede realizarse de forma progresiva a medida que avanzamos por los niveles y se vuelvan necesarios. Aquí utilizamos nuestros controladores para cargar y almacenar los sonidos e imágenes con los siguientes nombres:

Sound.load('shoot', 'sound/fire.mp3');          
Sound.load('crash', 'sound/crash.mp3');      
Graphics.load('tileset', 'img/tileSet.png', animateScene);
  • Sound.load(‘shoot’, ‘sound/fire.mp3’): (shoot) Es el sonido que se activa cuando un disparo del jugador colisiona con un meteorito.
  • Sound.load(‘crash’, ‘sound/crash.mp3’): (crash) Es el sonido que se activa cuando la nave del jugador colisiona con un meteorito.
  • Graphics.load(‘tileset’, ‘img/tileSet.png’, animateScene): (tileset) Es el tileset que contiene todas las imágenes utilizadas en el juego.

Los nombres utilizados para almacenar los sonidos y las imágenes deben ser los mismos que utilizaremos para referirnos a ellos posteriormente dentro del videojuego. Recuerda que nuestra librería gráfica cuenta con un cargador de imágenes en JavaScript que permite especificar una función de «callback» que se ejecutará una vez que todas las imágenes estén cargadas, lo que permite iniciar el juego en ese momento.

Bucle de animación en Vanilla JS

El bucle de animación es el corazón de nuestro videojuego realizado con Vanilla JavaScript, gestionando la lógica y el flujo del juego en cada fotograma. Utiliza requestAnimationFrame para asegurar una animación suave y eficiente, llamando repetidamente a la función animateScene que actualiza y dibuja el estado actual del juego. Dentro de esta función, el juego cambia de estado según el valor de Game.status, permitiendo manejar diferentes fases del juego como la pantalla de inicio, el juego en sí, el estado de pausa y el fin del juego. Cada estado ejecuta su propio conjunto de instrucciones para actualizar la pantalla y gestionar las interacciones del usuario.

function animateScene() {	
  switch(Game.status) {
    case  'Splash'  : Instrucciones ...
                             break;
    case  'Reset'    : Game.reset();
                             break;
    case  'Play'      : Instrucciones ...
                             break;
    case  'Crash'   : Instrucciones ...
                             break;    
    case  'GameOver': Instrucciones ...
                             break;                         
    case  'Pause'   : Instrucciones ...
                             break;
  }
  requestAnimationFrame(animateScene);
}

Ahora, veamos una visión general de cada uno de los estados del juego.

Splash

case  'Splash' : 
  Graphics.clearScreen();
  Graphics.text({y: 80, size: 40, color: 'orange', text: 'Meteor Rain'});
  Graphics.fillText({y: 140, size: 22, text: 'Elimina todos los meteoritos'});
  Graphics.fillText({y: 170, size: 22, text: 'sin chocar con ellos'});
  Graphics.fillText({y: 230, size: 20, color: 'orange', text: 'P: Pausa'});
  Graphics.fillText({y: 260, size: 20, color: 'orange', text: 'ESPACIO: Disparo'});
  Graphics.fillText({y: 290, size: 20, color: 'orange', text: 'CURSORES: Movimiento'});
  Graphics.text({y: 380, size: 50, text: 'Iniciar juego'});
  Graphics.text({y: 460, size: 50, text: '(y/n)'});
  Graphics.fillText({x: Graphics.width - 20, y: Graphics.height - 20, size: 20, color: 'orange', align: 'right', text: 'FashehLabs.com'});
  if (Keyboard.click('y') || Keyboard.click('Y')) {
    Game.status = 'Reset';                                  
    Game.addPlayer();   
    Game.init();                                                      
  }
  if (Keyboard.click('n') || Keyboard.click('N')) window.close();
break;

En el estado 'Splash', el juego muestra una pantalla de bienvenida o menú principal. Esta pantalla proporciona información sobre el juego y las instrucciones básicas para el jugador. Se limpia la pantalla y se muestran varios textos, incluyendo el título del juego, instrucciones sobre cómo jugar, y las teclas de control. Además, se muestra un mensaje indicando cómo iniciar el juego y las opciones para cerrar la ventana del navegador. Si el jugador presiona la tecla 'y' o 'Y', el juego cambia al estado 'Reset', donde se inicializa el juego y se prepara para comenzar. Si el jugador presiona la tecla 'n' o 'N', se cierra la ventana del navegador.

Reset

case  'Reset' : 
  Game.reset();
  Game.status = 'Play';
break;

En el estado 'Reset', el juego restablece todos los elementos y configuraciones a su estado inicial mediante el método Game.reset(). Esto incluye la reinicialización de las posiciones y estados de los objetos del juego, como el jugador, estrellas y enemigos. Después de realizar el restablecimiento, el juego cambia al estado 'Play', lo que indica que está listo para comenzar la partida y entrar en el ciclo de juego activo.

Play

case  'Play' : 
  Game.update();
  Game.addStars();
  Game.addEnemys();
  Game.collisions();
  Game.checkKeyboard();
  Graphics.clearScreen();
  Game.draw();
  if (Keyboard.click('p') || Keyboard.click('P')) {
    Graphics.text({size: 60, text: 'Pausa'});
    Game.status = 'Pause';
  }
break;

En el estado 'Play', el juego se encuentra en su fase activa. Primero, el juego actualiza el estado de todos los elementos mediante el método Game.update(). Luego, se añaden nuevas estrellas y enemigos a la pantalla con Game.addStars() y Game.addEnemys(), respectivamente. Se comprueban las colisiones entre los objetos del juego con Game.collisions(), y se gestionan las entradas del teclado con Game.checkKeyboard(). A continuación, se limpia la pantalla con Graphics.clearScreen() para preparar el lienzo para el siguiente dibujo. Finalmente, el estado actual del juego se dibuja en pantalla mediante Game.draw(). Si el jugador presiona la tecla ‘P’, se muestra el mensaje de «Pausa» y el estado del juego cambia a 'Pause'.

Crash

case  'Crash' : 
  Graphics.clearScreen();
  Game.draw();
  Graphics.text({size: 60, text: 'Crash !!'});
  Graphics.fillText({y: 380, size: 30, text: 'Enter'});
  if (Keyboard.click('Enter')) Game.crash();
break;  

En el estado 'Crash', la pantalla se limpia utilizando Graphics.clearScreen(), preparando el área para mostrar el mensaje de colisión. Luego, se dibuja el estado actual del juego con Game.draw(), lo que incluye cualquier gráfico o elemento visual que esté presente en ese momento. Se muestra un mensaje de «Crash !!» en la pantalla para indicar que la nave ha colisionado. También se muestra un mensaje adicional solicitando al jugador que presione la tecla ‘Enter’. Si el jugador presiona ‘Enter’, se llama al método Game.crash() para gestionar las acciones posteriores al choque, como reducir las vidas del jugador o cambiar el estado del juego según sea necesario.

GameOver

case  'GameOver' : 
  Graphics.clearScreen();
  Game.draw();
  Graphics.fillText({size: 50, text: 'Game Over'});
  Graphics.text({y: 380, size: 40, text: 'Repetir ?'});
  Graphics.text({y: 460, size: 40, text: '(y/n)'});
  if (Keyboard.click('y') || Keyboard.click('Y')) Game.status = 'Splash';   
  if (Keyboard.click('n') || Keyboard.click('N')) window.close();
break;  

En el estado 'GameOver', la pantalla se limpia primero con Graphics.clearScreen() para eliminar cualquier gráfico o texto anterior. Luego, se dibuja el estado actual del juego utilizando Game.draw(), que muestra la última imagen del juego antes del final. Se muestra un mensaje grande de «Game Over» para indicar que el juego ha terminado. A continuación, se presentan opciones al jugador: «Repetir ?» y «(y/n)» para solicitar si desea reiniciar el juego o cerrarlo. Si el jugador presiona ‘y’ o ‘Y’, se cambia el estado del juego a 'Splash', que reinicia la pantalla de inicio. Si el jugador presiona ‘n’ o ‘N’, se cierra la ventana del juego mediante window.close().

Pause

case  'Pause' : 
  if (Keyboard.click('p') || Keyboard.click('P')) Game.status = 'Play';
break;

En el estado 'Pause', el juego está en pausa y no se realizan actualizaciones ni se muestran gráficos de juego. El juego se mantiene en este estado hasta que el jugador presiona la tecla ‘p’ o ‘P’. Al detectar esta acción, el estado del juego se cambia a 'Play', reanudando el juego y permitiendo que se reanude la actualización y el dibujo de los elementos del juego.

Videojuegos con Vanilla JavaScript

En este artículo, hemos desarrollado un videojuego básico utilizando Vanilla JavaScript. A través del proceso, hemos aprendido y aplicado varios conceptos fundamentales en la programación de videojuegos, entre los que podemos destacar:

  • El proyecto está organizado en carpetas para gestionar eficazmente los recursos del juego.
  • Antes de iniciar el juego, es esencial cargar los recursos como imágenes y sonidos para asegurar una ejecución fluida.
  • El objeto Game es la pieza central del videojuego y está diseñado para gestionar la lógica del juego, separando responsabilidades y facilitando el desarrollo y mantenimiento. (Aunque es sólo un ejemplo básico, no es la forma correcta de programar un videojuego).
  • Las diferencias entre init(), que establece las condiciones iniciales del juego al empezar una nueva partida, y reset() que limpia el estado actual del juego para permitir un nuevo inicio sin cambiar los parámetros básicos actuales.
  • La forma de añadir nuevos elementos al juego con addPlayer(), addStars() y addEnemys().
  • La gestión de las entradas del teclado y su aplicación a las propiedades y acciones del jugador.
  • Cómo manejar diferentes listados de elementos gráficos para actualizar sus propiedades, dibujarlos en pantalla y detectar colisiones entre ellos.
  • Cómo manejar la colisión de la nave del jugador, reduciendo vidas y actualizando el estado del juego.
  • Cómo mostrar en pantalla la Interfaz del juego con información sobre el nivel, puntuación, vidas restantes e incluso el número total de elementos en el juego.

Conclusiones del videojuego en Vanilla JS

Aunque este código es una muestra básica para ilustrar el funcionamiento de los controladores y la lógica de un videojuego en Vanilla JavaScript, el desarrollo de juegos más complejos requiere una estructura más avanzada y una planificación más detallada. Sin embargo, con este ejemplo podemos ver el gran potencial del elemento <canvas> y su API gráfica, que permite manejar cientos de elementos gráficos a 60 fotogramas por segundo o más, dependiendo de tu hardware.

Si te acabas de interesar por la programación de videojuegos con Vanilla JavaScript o JavaScript puro, sin duda este artículo te será de gran ayuda. Y si deseas profundizar más en el tema, en este blog encontrarás numerosos artículos relacionados, así como mis libros sobre Programación de Videojuegos con JavaScript. Lo que sin duda es un buen punto de partida.

¡ Espero que este artículo sea de vuestro interés !

Deja un comentario