This article is currently in the process of being translated into Spanish (~99% done).
In this article series, we're building a complete Snake game from scratch. It makes sense to start with the Introduction and then work your way through the articles one by one, to get the full understanding.
If you want to get the complete source code for the game at once, to get started modifying and learning from it right now, consider downloading all our samples!
Collision Detection
Ahora que hemos implementado el área de juego, la comida y la serpiente, así como el movimiento continuo de la serpiente, solo necesitamos una última cosa para que se vea y actúe como un juego real: Detección de colisión . El concepto evoluciona en torno a verificar constantemente si nuestra Serpiente golpeó algo y actualmente lo necesitamos para dos propósitos: Para ver si la Serpiente acaba de comer algo de comida o si golpeó un obstáculo (la pared o su propia cola).
El método DoCollisionCheck ()
La detección de colisión se realizará en un método llamado DoCollisionCheck () , por lo que debemos implementarlo. Así es como debería verse actualmente:
private void DoCollisionCheck()
{
SnakePart snakeHead = snakeParts[snakeParts.Count - 1];
if((snakeHead.Position.X == Canvas.GetLeft(snakeFood)) && (snakeHead.Position.Y == Canvas.GetTop(snakeFood)))
{
EatSnakeFood();
return;
}
if((snakeHead.Position.Y < 0) || (snakeHead.Position.Y >= GameArea.ActualHeight) ||
(snakeHead.Position.X < 0) || (snakeHead.Position.X >= GameArea.ActualWidth))
{
EndGame();
}
foreach(SnakePart snakeBodyPart in snakeParts.Take(snakeParts.Count - 1))
{
if((snakeHead.Position.X == snakeBodyPart.Position.X) && (snakeHead.Position.Y == snakeBodyPart.Position.Y))
EndGame();
}
}
Según lo prometido, hacemos dos comprobaciones: primero vemos si la posición actual de la cabeza de la serpiente coincide con la posición del alimento actual. Si es así, llamamos al método EatSnakeFood () (más sobre eso más adelante). Luego verificamos si la posición de la cabeza de la serpiente excede los límites de GameArea, para ver si la serpiente está saliendo de uno de los bordes. Si es así, llamamos al método EndGame(). Finalmente, verificamos si la cabeza de la serpiente coincide con una de las posiciones de la parte del cuerpo; si lo hace, la serpiente simplemente colisionó con su propia cola, que también finalizará el juego, con una llamada a EndGame () .
El método EatSnakeFood ()
El método EatSnakeFood () es responsable de hacer un par de cosas, porque tan pronto como la serpiente come el alimento actual, necesitamos agregar uno nuevo, en una nueva ubicación, así como ajustar la puntuación, la longitud de la serpiente y la velocidad actual del juego. Para la puntuación, debemos declarar una nueva variable local llamada currentScore :
public partial class SnakeWPFSample : Window
{
....
private int snakeLength;
private int currentScore = 0;
....
Con eso en su lugar, agregue el método EatSnakeFood ():
private void EatSnakeFood()
{
snakeLength++;
currentScore++;
int timerInterval = Math.Max(SnakeSpeedThreshold, (int)gameTickTimer.Interval.TotalMilliseconds - (currentScore * 2));
gameTickTimer.Interval = TimeSpan.FromMilliseconds(timerInterval);
GameArea.Children.Remove(snakeFood);
DrawSnakeFood();
UpdateGameStatus();
}
Como se mencionó, varias cosas suceden aquí:
- Incrementamos las variables snakeLength y currentScore en una para reflejar el hecho de que la serpiente acaba de atrapar un pedazo de comida.
- Ajustamos el Intervalo de gameTickTimer , usando la siguiente regla: El score actual se multiplica por 2 y luego se resta del intervalo actual (velocidad). Esto hará que la velocidad crezca exponencialmente junto con la longitud de la serpiente, haciendo que el juego sea cada vez más difícil. Hemos definido previamente un límite inferior para la velocidad, con la constante SnakeSpeedThreshold, lo que significa que la velocidad del juego nunca cae por debajo de un intervalo de 100 ms.
- Luego retiramos el pedazo de comida que acaba de consumir la serpiente y luego llamamos al método DrawSnakeFood () que agregará un nuevo pedazo de comida en una nueva ubicación.
- Finalmente, llamamos al método UpdateGameStatus (), que se ve así:
private void UpdateGameStatus()
{
this.Title = "SnakeWPF - Score: " + currentScore + " - Game speed: " + gameTickTimer.Interval.TotalMilliseconds;
}
Este método simplemente actualizará la propiedad Título de la Ventana para reflejar el puntuación actual y la velocidad del juego. Esta es una manera fácil de mostrar el estado actual, que se puede ampliar fácilmente más adelante si lo desea.
El método EndGame ()
También necesitamos un poco de código para ejecutar cuándo debe terminar el juego. Haremos esto desde el método EndGame () , que actualmente se llama desde el método DoCollisionCheck (). Como puede ver, actualmente es muy simple:
private void EndGame()
{
gameTickTimer.IsEnabled = false;
MessageBox.Show("Oooops, you died!\n\nTo start a new game, just press the Space bar...", "SnakeWPF");
}
Además de mostrar un mensaje al usuario sobre el desafortunado fallecimiento de nuestra amada serpiente, simplemente detenemos el gameTickTimer . Dado que este temporizador es lo que hace que sucedan todas las cosas en el juego, tan pronto como se detiene, también se detiene todo movimiento y dibujo.
Ajustes finales
Ahora estamos casi listos con el primer borrador de un juego Snake completamente funcional; de hecho, solo tenemos que hacer dos ajustes menores. Primero, debemos asegurarnos de que se llame al DoCollisionCheck () ; esto debería suceder como la última acción realizada en el método MoveSnake () , que implementamos anteriormente:
private void MoveSnake()
{
.....
//... and then have it drawn!
DrawSnake();
// Finally: Check if it just hit something!
DoCollisionCheck();
}
¡Ahora la detección de colisión se realiza tan pronto como la serpiente se haya movido! Ahora recuerda cómo te dije que implementamos una variante simple del método StartNewGame () . Necesitamos expandirlo un poco, para asegurarnos de que reiniciamos el puntaje cada vez que el juego se (re) inicia, ya que nosotros como un par de otras cosas. Por lo tanto, reemplace el método StartNewGame () con esta versión ligeramente extendida:
private void StartNewGame()
{
// Remove potential dead snake parts and leftover food...
foreach(SnakePart snakeBodyPart in snakeParts)
{
if(snakeBodyPart.UiElement != null)
GameArea.Children.Remove(snakeBodyPart.UiElement);
}
snakeParts.Clear();
if(snakeFood != null)
GameArea.Children.Remove(snakeFood);
// Reset stuff
currentScore = 0;
snakeLength = SnakeStartLength;
snakeDirection = SnakeDirection.Right;
snakeParts.Add(new SnakePart() { Position = new Point(SnakeSquareSize * 5, SnakeSquareSize * 5) });
gameTickTimer.Interval = TimeSpan.FromMilliseconds(SnakeStartSpeed);
// Draw the snake again and some new food...
DrawSnake();
DrawSnakeFood();
// Update status
UpdateGameStatus();
// Go!
gameTickTimer.IsEnabled = true;
}
Cuando comienza un nuevo juego, sucede lo siguiente:
- Dado que este podría no ser el primer juego, debemos asegurarnos de que se eliminen las sobras potenciales de un juego anterior: esto incluye todas las partes existentes de la serpiente, así como los restos de comida.
- También necesitamos restablecer algunas de las variables a su configuración inicial, como la puntuación , la longitud , la dirección y la velocidad del temporizador. También agregamos la cabeza de serpiente inicial (que se expandirá automáticamente mediante el método MoveSnake ()).
- Luego llamamos a los métodos DrawSnake () y DrawSnakeFood () para reflejar visualmente que se inició un nuevo juego.
- Llamamos al método UpdateGameStatus ().
- Y finalmente, estamos listos para comenzar el gameTickTimer : comenzará a funcionar de inmediato, básicamente, poniendo el juego en movimiento.
Resumen
Si lograste completar esta serie de artículos: felicidades, ¡acabas de crear tu primer juego de WPF! Disfruta de tu arduo trabajo ejecutando tu proyecto, presionando la tecla Espacio y comienza a jugar, incluso en esta implementación muy simple, ¡Serpiente (Snake) es un juego divertido y adictivo!