TOC

This article is currently in the process of being translated into French (~25% done).

Création d'un jeu : SnakeWPF:
Chapter introduction:

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!

Détection des collisions

Maintenant que la zone de jeu, la nourriture, le Snake, ainsi que ses déplacements, sont implémentés, nous n'avons besoin que d'une dernière chose pour que cela ressemble et se joue vraiment comme un jeu : Une détection des collisions. Le concept se base sur une vérification continuelle de la position du Snake, pour savoir si il a percuté quelque chose et nous en avons actuellement besoin pour deux raisons : Pour savoir si le Snake vient de manger ou si il vient de toucher un obstacle (un mur ou bien sa propre queue).

La méthode DoCollisionCheck()

La détection des collisions sera effectué par la méthode DoCollisionCheck(), nous avons donc besoin de l'implémenter. Voici ce à quoi elle devrait ressembler :

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();
    }
}

As promised, we do two checks: First we see if the current position of the snake's head matches the position of the current piece of food. If it does, we call the EatSnakeFood() method (more on that later). We then check if the position of the snake's head exceeds the boundaries of the GameArea, to see if the snake is on its way out of one of the borders. If it is, we call the EndGame() method. Finally, we check if the snake's head matches one of the body part positions - if it does, the snake just collided with its own tail, which will end the game as well, with a call to EndGame().

The EatSnakeFood() method

The EatSnakeFood() method is responsible for doing a couple of things, because as soon as the snake eats the current piece of food, we need to add a new one, in a new location, as well as adjust the score, the length of the snake and the current game speed. For the score, we need to declare a new local variable called currentScore:

public partial class SnakeWPFSample : Window  
{  
    ....  
    private int snakeLength;  
    private int currentScore = 0;  
    ....

With that in place, add the EatSnakeFood() method:

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();
}

Comme dit précédemment, plusieurs choses se passent ici :

  • On incrémente snakeLength et currentScore de un pour refléter le fait que le Snake vient de manger une unité de nourriture.
  • On ajuste l'intervalle du gameTickTimer, d'après la règle suivante : Le currentScore est multiplié par deux puis soustrait à la valeur actuelle de l'intervalle (la vitesse). Cela va permettre une évolution exponentielle de la vitesse suivant la longueur du Snake, rendant le jeu de plus en plus difficile. On a défini précédemment une limite inférieur pour la vitesse, avec la constante SnakeSpeedThreshold, donc la vitesse de jeu ne pourra jamais descendre en dessous d'un intervalle de 100ms.
  • We then remove the piece of food just consumed by the snake and then we call the DrawSnakeFood() method which will add a new piece of food in a new location.
  • Finally, we call the UpdateGameStatus() method, which looks like this:
private void UpdateGameStatus()
{
    this.Title = "SnakeWPF - Score: " + currentScore + " - Game speed: " + gameTickTimer.Interval.TotalMilliseconds;
}

This method will simply update the Title property of the Window to reflect the current score and game speed. This is an easy way of showing the current status, which can easily be extended later on if desired.

The EndGame() method

We also need a little bit of code to execute when the game should end. We'll do this from the EndGame() method, which is currently called from the DoCollisionCheck() method. As you can see, it's currently very simple:

private void EndGame()
{
    gameTickTimer.IsEnabled = false;
    MessageBox.Show("Oooops, you died!\n\nTo start a new game, just press the Space bar...", "SnakeWPF");
}

Besides showing a message to the user about the unfortunate passing of our beloved snake, we simply stop the gameTickTimer. Since this timer is what causes all things to happen in the game, as soon as it's stopped, all movement and drawing also stops.

Final adjustments

We're now almost ready with the first draft of a fully-functional Snake game - in fact, we just need to make two minor adjustments. First, we need to make sure that the DoCollisionCheck() is called - this should happen as the last action performed in the MoveSnake() method, which we implemented previously:

private void MoveSnake()
{
    .....
   
    //... and then have it drawn!
    DrawSnake();
    // Finally: Check if it just hit something!
    DoCollisionCheck();    
}

Now collision detection is performed as soon as the snake has moved! Now remember how I told you that we implemented a simple variant of the StartNewGame() method? We need to expand it a bit, to make sure that we reset the score each time the game is (re)started, as we as a couple of other things. So, replace the StartNewGame() method with this slightly extended version:

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;
}

When a new game starts, the following things now happens:

  • Since this might not be the first game, we need to make sure that any potential leftovers from a previous game is removed: This includes all existing parts of the snake, as well as leftover food.
  • We also need to reset some of the variables to their initial settings, like the score, the length, the direction and the speed of the timer. We also add the initial snake head (which will be automatically expanded by the MoveSnake() method).
  • We then call the DrawSnake() and DrawSnakeFood() methods to visually reflect that a new game was started.
  • The we call the UpdateGameStatus() method.
  • And finally, we're ready to start the gameTickTimer - it will immediately start ticking, basically setting the game in motion.

Summary

If you made it all the way through this article series: congratulations - you just built your first WPF game! Enjoy all your hard labor by running your project, pressing the Space key and start playing - even in this very simple implementation, Snake is a fun and addictive game!

This article has been fully translated into the following languages: Is your preferred language not on the list? Click here to help us translate this article into your language!