Piense como un programador: cómo construir Snake usando solo JavaScript, HTML y CSS

Hola

La bienvenida a bordo. Hoy nos embarcaremos en una aventura emocionante, donde haremos nuestro propio juego de serpientes . Aprenderá a resolver un problema dividiéndolo en pasos más pequeños y simples. Al final de este viaje, habrás aprendido algunas cosas nuevas y te sentirás seguro de explorar más por tu cuenta.

Si eres nuevo en la programación, te recomiendo consultar freeCodeCamp. Es un gran lugar para aprender ... lo adivinaste ... gratis. Así es como empecé

Está bien, está bastante bien jugando, ¿estás listo para comenzar?

Puede encontrar el código final aquí y una demostración en vivo aquí.

Empezando

Comencemos creando un archivo "snake.html" que contendrá todo nuestro código.

Como se trata de un archivo HTML, lo primero que necesitamos es la declaración . En snake.html escriba lo siguiente:

Genial, ahora adelante y abra snake.html en su navegador preferido. ¡Deberías poder ver Bienvenido a Snake!

snake.html abierto en cromo

Hemos tenido un buen comienzo

Creando el lienzo

Para poder crear nuestro juego, tenemos que hacer uso de HTML . Esto es lo que se usa para dibujar gráficos usando JavaScript.

Reemplace el mensaje de bienvenida en snake.html con lo siguiente:

 

La identificación es lo que identifica el lienzo y siempre debe especificarse. Lo usaremos para acceder al lienzo más tarde. El ancho y la altura son las dimensiones del lienzo, y también deben especificarse. En este caso, 300 x 300 píxeles.

Su archivo snake.html ahora debería verse así.

Si actualiza la página de su navegador donde abrió anteriormente snake.html, ahora verá una página en blanco. Esto se debe a que, por defecto, el lienzo está vacío y no tiene fondo. Vamos a arreglar eso.

Dale al lienzo un color de fondo y un borde

Para hacer visible nuestro lienzo, podemos darle un borde escribiendo algún código JavaScript. Para hacer eso, necesitamos insertar etiquetas después de , donde irá todo nuestro código JavaScript.

Si coloca la etiqueta . Actualice su código de la siguiente manera.

Primero obtenemos el elemento de lienzo usando el id (gameCanvas) que especificamos anteriormente. Luego obtenemos el contexto del lienzo "2d", lo que significa que dibujaremos en el espacio 2D.

Finalmente dibujamos un rectángulo blanco de 300 x 300 con un borde negro. Esto cubre todo el lienzo, comenzando desde la esquina superior izquierda (0, 0).

Si vuelve a cargar snake.html en su navegador, ¡debería ver un cuadro blanco con un borde negro! Buen trabajo, ¡tenemos un lienzo que podemos usar para crear nuestro juego de serpientes! ¡Al próximo desafío!

Representando a nuestra serpiente

Para que nuestro juego de serpientes funcione, necesitamos saber la ubicación de la serpiente en el lienzo. Para hacer eso, podemos representar a la serpiente como una matriz de coordenadas. Por lo tanto, para crear una serpiente horizontal en el medio del lienzo (150, 150) podemos escribir lo siguiente:

dejar serpiente = [
  {x: 150, y: 150},
  {x: 140, y: 150},
  {x: 130, y: 150},
  {x: 120, y: 150},
  {x: 110, y: 150},
];

Observe que la coordenada y para todas las partes es siempre 150. La coordenada x de cada parte es -10px (a la izquierda) de la parte anterior. El primer par de coordenadas en la matriz {x: 150, y: 150} representa la cabeza a la derecha de la serpiente.

Esto se aclarará cuando dibujemos la serpiente en la siguiente sección.

Creando y dibujando nuestra serpiente

Para mostrar la serpiente en el lienzo, podemos escribir una función para dibujar un rectángulo para cada par de coordenadas.

función drawSnakePart (snakePart) {
  ctx.fillStyle = 'verde claro';
  ctx.strokestyle = 'verde oscuro';
  ctx.fillRect (snakePart.x, snakePart.y, 10, 10);
  ctx.strokeRect (snakePart.x, snakePart.y, 10, 10);
}

A continuación, podemos crear otra función que imprima las partes en el lienzo.

función drawSnake () {
  snake.forEach (drawSnakePart);
}

Nuestro archivo snake.html ahora debería verse así:

Si actualiza la página de su navegador ahora verá una serpiente verde en el medio del lienzo. ¡Increíble!

Permitir que la serpiente se mueva horizontalmente

A continuación, queremos darle a la serpiente la capacidad de moverse. Pero cómo hacemos eso?

Bueno, para hacer que la serpiente se mueva un paso (10px) a la derecha, podemos aumentar la coordenada x de cada parte de la serpiente en 10px (dx = + 10px). Para hacer que la serpiente se mueva hacia la izquierda, podemos disminuir la coordenada x de cada parte de la serpiente en 10px (dx = -10).

dx es la velocidad horizontal de la serpiente.

Crear una serpiente que se haya movido 10px a la derecha debería verse así

Cree una función llamada advanceSnake que usaremos para actualizar la serpiente.

función advanceSnake () {
  const head = {x: serpiente [0] .x + dx, y: serpiente [0] .y};
  serpiente.unshift (cabeza);
  snake.pop ();
}

Primero creamos una nueva cabeza para la serpiente. Luego agregamos la nueva cabeza al comienzo de la serpiente usando unshift y eliminamos el último elemento de la serpiente usando pop. De esta manera, todas las otras partes de la serpiente se colocan en su lugar como se muestra arriba.

Boom , te estás acostumbrando a esto.

Permitir que la serpiente se mueva verticalmente

Para mover nuestra serpiente hacia arriba y hacia abajo, no podemos alterar todas las coordenadas y en 10px. Eso desplazaría a toda la serpiente de arriba abajo.

En cambio, podemos alterar la coordenada y de la cabeza. Disminuirlo en 10px para mover la serpiente hacia abajo, y aumentarlo en 10px para mover la serpiente hacia arriba. Esto hará que la serpiente se mueva correctamente.

Afortunadamente, debido a la forma en que escribimos la función advanceSnake, esto es muy fácil de hacer. Dentro de AdvanceSnake, actualice la cabeza para aumentar también la coordenada y de la cabeza por dy.

const head = {x: serpiente [0] .x + dx, y: serpiente [0] .y + dy};

Para probar cómo funciona nuestra función advanceSnake, podemos llamarla temporalmente antes de la función drawSnake.

// Avanzar paso a la derecha
advanceSnake ()
// Cambia la velocidad vertical a 0
dx = 0;
// Cambia la velocidad horizontal a 10
dy = -10;
// Avanza un paso
advanceSnake ();
// Dibuja una serpiente en el lienzo
drawSnake ();

Así es como se ve nuestro archivo snake.html hasta ahora.

Al actualizar el navegador, podemos ver que nuestra serpiente se ha movido. ¡Éxito!

Refactorizando nuestro código

Antes de continuar, vamos a refactorizar y mover el código que dibuja el lienzo dentro de una función. Esto nos ayudará en la siguiente sección.

"La refactorización de código es el proceso de reestructurar el código de computadora existente, sin cambiar su comportamiento externo".
función clearCanvas () {
  ctx.fillStyle = "blanco";
  ctx.strokeStyle = "negro";
  ctx.fillRect (0, 0, gameCanvas.width, gameCanvas.height);
  ctx.strokeRect (0, 0, gameCanvas.width, gameCanvas.height);
}

¡Estamos dando grandes pasos!

Hacer que nuestra serpiente se mueva automáticamente

Bien, ahora que hemos reestructurado con éxito nuestro código, podemos hacer que nuestra serpiente se mueva automáticamente.

Anteriormente, para probar que nuestra función advanceSnake funcionaba, la llamamos dos veces. Una vez para hacer que la serpiente se mueva hacia la derecha, y una vez para hacer que la serpiente se mueva hacia arriba.

Por lo tanto, si quisiéramos hacer que la serpiente se moviera cinco pasos hacia la derecha, llamaríamos a advanceSnake () cinco veces seguidas.

clearCanvas ();
advanceSnake ();
advanceSnake ();
advanceSnake ();
advanceSnake ();
advanceSnake ();
drawSnake ();

Pero, llamarlo cinco veces seguidas como se muestra arriba, hará que la serpiente salte 50px hacia adelante.

En cambio, queremos hacer que la serpiente parezca avanzar paso a paso.

Para hacer eso, podemos agregar un ligero retraso entre cada llamada, usando setTimeout. También debemos asegurarnos de llamar a drawSnake cada vez que llame a advanceSnake. Si no lo hacemos, no podremos ver los pasos intermedios que muestran a la serpiente moviéndose.

setTimeout (function onTick () {
  clearCanvas ();
  advanceSnake ();
  drawSnake ();
}, 100);
setTimeout (function onTick () {
  clearCanvas ();
  advanceSnake ();
  drawSnake ();
}, 100);
...
drawSnake ();

Observe cómo también llamamos a clearCanvas () dentro de cada setTimeout. Esto es para eliminar todas las posiciones anteriores de la serpiente que dejarían un rastro.

Aunque, hay un problema con el código anterior. No hay nada aquí para decirle al programa que tiene que esperar a setTimeout antes de pasar al siguiente setTimeout. Esto significa que la serpiente aún saltará 50 píxeles hacia adelante, pero después de un ligero retraso.

Para arreglar eso, tenemos que ajustar nuestro código dentro de las funciones, llamando a una función a la vez.

paso uno();
    
función stepOne () {
  setTimeout (function onTick () {
    clearCanvas ();
    advanceSnake ();
    drawSnake ();
   // Llama a la segunda función
   segundo paso();
  }, 100)
}
función stepTwo () {
  setTimeout (function onTick () {
    clearCanvas ();
    advanceSnake ();
    drawSnake ();
    // Llama a la tercera función
    Paso tres();
  }, 100)
}
...

¿Cómo hacemos que nuestra serpiente siga moviéndose? En lugar de crear un número infinito de funciones que se llaman entre sí, podemos crear una función principal y llamarla una y otra vez.

función main () {
  setTimeout (function onTick () {
    clearCanvas ();
    advanceSnake ();
    drawSnake ();
    // Llamar a main nuevamente
    principal();
  }, 100)
}

Voilà! Ahora tenemos una serpiente que seguirá moviéndose hacia la derecha. Aunque, una vez que llega al final del lienzo, continúa su viaje infinito hacia lo desconocido . Arreglaremos eso a su debido tiempo, paciencia joven padawan. .

Cambiar la dirección de la serpiente

Nuestra siguiente tarea es cambiar la dirección de la serpiente cuando se presiona una de las teclas de flecha. Agregue el siguiente código después de la función drawSnakePart.

función changeDirection (evento) {
  const LEFT_KEY = 37;
  const RIGHT_KEY = 39;
  const UP_KEY = 38;
  const DOWN_KEY = 40;
  const keyPressed = event.keyCode;
  const goingUp = dy === -10;
  const goingDown = dy === 10;
  const goingRight = dx === 10;
  const goingLeft = dx === -10;
  if (keyPressed === LEFT_KEY &&! goingRight) {
    dx = -10;
    dy = 0;
  }
  if (keyPressed === UP_KEY &&! goingDown) {
    dx = 0;
    dy = -10;
  }
  if (keyPressed === RIGHT_KEY &&! goingLeft) {
    dx = 10;
    dy = 0;
  }
  if (keyPressed === DOWN_KEY &&! goingDown) {
    dx = 0;
    dy = 10;
  }
}

No hay nada complicado aquí. Verificamos si la tecla presionada coincide con una de las teclas de flecha. Si lo hace, cambiamos la velocidad vertical y horizontal como se describió anteriormente.

Observe que también verificamos si la serpiente se mueve en la dirección opuesta a la nueva dirección prevista. Esto es para evitar que nuestra serpiente se invierta, por ejemplo, cuando presiona la tecla de flecha derecha cuando la serpiente se mueve hacia la izquierda.

Serpiente marcha atrás

Para conectar changeDirection a nuestro juego, podemos usar addEventListener en el documento para "escuchar" cuando se presiona una tecla. Entonces podemos llamar a changeDirection con el evento keydown. Agregue el siguiente código después de la función principal.

document.addEventListener ("keydown", changeDirection)

Ahora debería poder cambiar la dirección de la serpiente con las cuatro teclas de flecha. ¡Buen trabajo, estás en llamas!

A continuación, veamos cómo podemos generar alimentos y cultivar nuestra serpiente.

Generando comida para la serpiente

Para nuestra comida de serpiente, tenemos que generar un conjunto aleatorio de coordenadas. Podemos usar una función auxiliar randomTen para producir dos números. Uno para la coordenada x y otro para la coordenada y.

También tenemos que asegurarnos de que la comida no se encuentre donde está la serpiente actualmente. Si es así, tenemos que generar una nueva ubicación de alimentos.

función randomTen (min, max) {
  return Math.round ((Math.random () * (max-min) + min) / 10) * 10;
}
función createFood () {
  foodX = randomTen (0, gameCanvas.width - 10);
  foodY = randomTen (0, gameCanvas.height - 10);
  snake.forEach (función isFoodOnSnake (parte) {
    const foodIsOnSnake = part.x == foodX && part.y == foodY
    if (foodIsOnSnake)
      createFood ();
  });
}

Luego tenemos que crear una función para dibujar la comida en el lienzo.

función drawFood () {
 ctx.fillStyle = 'rojo';
 ctx.strokestyle = 'darkred';
 ctx.fillRect (foodX, foodY, 10, 10);
 ctx.strokeRect (foodX, foodY, 10, 10);
}

Finalmente podemos llamar a createFood antes de llamar a main. No olvides actualizar también main para usar la función drawFood.

función main () {
  setTimeout (function onTick () {
    clearCanvas ();
    drawFood ()
    advanceSnake ();
    drawSnake ();
    principal();
  }, 100)
}

Creciendo la serpiente

Hacer crecer nuestra serpiente es simple. Podemos actualizar nuestra función advanceSnake para verificar si la cabeza de la serpiente está tocando la comida. Si es así, podemos omitir la eliminación de la última parte de la serpiente y crear una nueva ubicación de alimentos.

función advanceSnake () {
  const head = {x: serpiente [0] .x + dx, y: serpiente [0] .y};
  serpiente.unshift (cabeza);
  const didEatFood = serpiente [0] .x === comidaX && serpiente [0] .y === comidaY;
  if (didEatFood) {
    createFood ();
  } más {
    snake.pop ();
  }
}

Hacer un seguimiento de la puntuación

Para que el juego sea más agradable para el jugador, también podemos agregar una puntuación que aumenta cuando la serpiente come comida.

Cree una nueva puntuación variable y configúrela en 0 después de la declaración de la serpiente.

dejar puntaje = 0;

A continuación, agregue un nuevo div con una id "puntuación" antes del lienzo. Podemos usar esto para mostrar el puntaje.

0

Finalmente, actualice advanceSnake para aumentar y mostrar la puntuación cuando la serpiente se coma la comida.

función advanceSnake () {
  ...
  if (didEatFood) {
    puntuación + = 10;
    document.getElementById ('score'). innerHTML = score;
    createFood ();
  } más {
    ...
  }
}

Uff, eso fue bastante, pero hemos recorrido un largo camino

Termina el juego

Queda una pieza final, y es terminar el juego . Para hacer eso, podemos crear una función didGameEnd que devuelva verdadero cuando el juego haya terminado o falso de lo contrario.

function didGameEnd () {
  for (let i = 4; i 
    if (didCollide) devuelve verdadero
  }
  const hitLeftWall = serpiente [0] .x <0;
  const hitRightWall = serpiente [0] .x> gameCanvas.width - 10;
  const hitToptWall = serpiente [0] .y <0;
  const hitBottomWall = serpiente [0] .y> gameCanvas.height - 10;
  volver hitLeftWall ||
         hitRightWall ||
         hitToptWall ||
         hitBottomWall
}

Primero verificamos si la cabeza de la serpiente toca otra parte de la serpiente y devuelve verdadero si lo hace.

Observe que comenzamos nuestro ciclo desde el índice 4. Hay dos razones para eso. La primera es que didCollide se evaluaría inmediatamente como verdadero si el índice fuera 0, por lo que el juego terminaría. La segunda es que es imposible que las tres primeras partes se toquen entre sí.

A continuación, verificamos si la serpiente golpeó alguna de las paredes del lienzo y devuelve verdadero si lo hizo, de lo contrario, devolvemos falso.

Ahora podemos regresar temprano en nuestra función principal si didEndGame devuelve verdadero, terminando así el juego.

función main () {
  if (didGameEnd ()) return;
  ...
}

Nuestro snake.html ahora debería verse así:

Ahora tienes un juego de serpientes en funcionamiento que puedes jugar y compartir con tus amigos. Pero antes de celebrar, veamos un problema final. Este será el último, lo prometo.

Errores furtivos

Si juegas el juego suficientes veces, podrías notar que a veces el juego termina inesperadamente. Este es un muy buen ejemplo de cómo los errores pueden colarse en nuestros programas y causar problemas .

Cuando ocurre un error, la mejor manera de resolverlo es tener primero una forma confiable de reproducirlo. Es decir, proponer los pasos precisos que conducen a un comportamiento inesperado. Luego debe comprender por qué causan el comportamiento inesperado y luego encontrar una solución.

Reproduciendo el error

En nuestro caso, los pasos para reproducir el error son los siguientes:

  • La serpiente se mueve hacia la izquierda.
  • El jugador presiona la tecla de flecha hacia abajo
  • El jugador presiona inmediatamente la tecla de flecha derecha (antes de que hayan transcurrido 100 ms)
  • El juego termina

Darle sentido al error

Analicemos lo que sucede paso a paso.

La serpiente se mueve hacia la izquierda

  • Velocidad horizontal, dx es igual a -10
  • la función principal se llama
  • Se llama a advancedSnake que avanza la serpiente -10px a la izquierda.

El jugador presiona la tecla de flecha hacia abajo

  • changeDirection se llama
  • keyPressed === DOWN_KEY && dy! goingUp se evalúa como verdadero
  • dx cambia a 0
  • dy cambia a +10

El jugador presiona inmediatamente la flecha hacia la derecha (antes de que hayan transcurrido 100 ms)

  • changeDirection se llama
  • keyPressed === RIGHT_KEY &&! goingLeft se evalúa como verdadero
  • dx cambia a +10
  • dy cambia a 0

El juego termina

  • La función principal se llama después de que hayan transcurrido 100 ms.
  • Se llama advanceSnake que avanza la serpiente 10px a la derecha.
  • const didCollide = serpiente [i] .x === serpiente [0] .x && serpiente [i] .y === serpiente [0] .y se evalúa como verdadero
  • didGameEnd devuelve verdadero
  • la función principal regresa temprano
  • El juego termina

Arreglando el error

Después de estudiar lo que sucedió, nos enteramos de que el juego terminó porque la serpiente se invirtió.

Esto se debe a que cuando el jugador presionó la flecha hacia abajo, dx se estableció en 0. Por lo tanto, se presionó la tecla === RIGHT_KEY &&! GoingLeft evaluado como verdadero y dx cambió a 10.

Es importante tener en cuenta que el cambio de dirección ocurrió antes de que transcurrieran 100 ms. Si transcurrieran 100 ms, entonces la serpiente habría dado un paso hacia abajo y no se habría revertido.

Para corregir nuestro error, debemos asegurarnos de que solo podamos cambiar la dirección después de que se haya llamado a main y advancedSnake. Podemos crear una variable ChangingDirection. Esto se establecerá en verdadero cuando se llama a changeDirection y en falso cuando se llama a advancedSnake.

Dentro de nuestra función changeDirection, podemos regresar temprano si changeDirection es verdadero.

función changeDirection (evento) {
  const LEFT_KEY = 37;
  const RIGHT_KEY = 39;
  const UP_KEY = 38;
  const DOWN_KEY = 40;
  if (ChangingDirection) devuelve;
  ChangingDirection = true;
  ...
}
función main () {
  setTimeout (function onTick () {
    ChangingDirection = false;
    
    ...
  }, 100)
}

Aquí está nuestra versión final de snake.html

Observe que también agregué algunos estilos entre las etiquetas . Es hacer que el lienzo y la puntuación aparezcan en el centro de la pantalla.

Conclusión

¡¡Felicidades!!

Hemos llegado al final de nuestro viaje. Espero que hayas disfrutado aprendiendo conmigo y ahora te sientas seguro de continuar con tu próxima aventura.

Pero no tiene que terminar aquí. Mi próximo artículo se centrará en ayudarlo a comenzar con el apasionante mundo del código abierto.

El código abierto es una excelente manera de aprender muchas cosas nuevas y conocer gente increíble. Es muy gratificante pero puede dar miedo al principio .

Para recibir una notificación cuando salga mi próximo artículo, ¡puedes seguirme!

Fue un placer estar en este viaje contigo.

Hasta la próxima.